9.1 モジュールの循環依存関係の作成
例えば、a.py
と b.py
という2つのファイルがあり、それぞれが次のように他をインポートしているとしましょう:
a.py
の場合:
import b
def f():
return b.x
print(f())
b.py
の場合:
import a
x = 1
def g():
print(a.f())
まずは a.py
をインポートしてみましょう:
import a
# 1
問題なく動作しました。ちょっと驚きかもしれません。結局のところ、モジュールはお互いに循環してインポートしているので、問題があるはずです。
答えは、循環インポートそのものはPythonでは問題ではないということです。もしモジュールがすでにインポートされている場合、Pythonは再インポートを試みないほど賢いです。しかし、どの時点で各モジュールが他で定義されている関数や変数にアクセスしようとするかによって、本当に問題に直面することがあります。
さて、最初の例に戻りますと、a.py
をインポートしたとき、b.py
のインポートに問題はありませんでした。なぜなら、b.py
がインポート時にa.py
から何かを必要としていなかったからです。b.py
内の唯一の a
への参照は a.f()
の呼び出しです。しかし、この呼び出しは g()
内で行われ、a.py
や b.py
の何も g()
を呼び出しません。なので、すべてはうまくいきます。
しかし、もし b.py
をインポートしようとしたら(先に a.py
をインポートせずに、つまり):
import b
Traceback (most recent call last):
File "<stdin>", line 1, in
File "b.py", line 1, in
import a
File "a.py", line 6, in
print(f())
File "a.py", line 4, in f
return b.x
AttributeError: 'module' object has no attribute 'x'
ここでの問題は、b.py
のインポート中に a.py
をインポートしようとし、それが f()
を呼び出し、b.x
にアクセスしようとすることです。しかし、b.x
はまだ定義されていません。ここからAttributeError
が発生します。
この問題の少なくとも一つの解決策は非常に簡単です。g()
の中で a.py
をインポートするように b.py
を変更するだけです:
x = 1
def g():
import a # これは g() が呼ばれたときにのみ評価されます
print(a.f())
これで、インポートしても問題ありません:
import b
b.g()
# 1 モジュール 'a' が 'print(f())' を最後に呼び出すため、1回目にプリントされる
# 1 2回目にプリントされるのは、これが 'g()' への我々の呼び出し
9.2 Pythonの標準ライブラリのモジュール名との衝突
Pythonの魅力の一つは、「すぐに使える」多数のモジュールがあることです。しかし、その結果、意識して気をつけなければ、モジュール名がPython付属の標準ライブラリのモジュール名と重複することがあります(例えば、email.py
という名前のモジュールがコード内にあると、同名の標準ライブラリのモジュールと衝突します)。
これは重大な問題を引き起こす可能性があります。例えば、あるモジュールがPythonの標準ライブラリのバージョンをインポートしようとしたときに、プロジェクト内に同名のモジュールがあると、標準ライブラリのモジュールの代わりに誤って自分のモジュールをインポートしてしまうことがあります。
したがって、Pythonの標準ライブラリのモジュールと同じ名前を使用しないよう注意することが重要です。プロジェクトのモジュール名を変更する方が、標準ライブラリのモジュール名の変更を依頼し、その承認を待つよりはるかに簡単です。
9.3 例外の可視性
次の main.py
ファイルを考えてみましょう:
import sys
def bar(i):
if i == 1:
raise KeyError(1)
if i == 2:
raise ValueError(2)
def bad():
e = None
try:
bar(int("1"))
except KeyError as e:
print('key error')
except ValueError as e:
print('value error')
print(e)
bad()
コードは正しく見えますし、動作するはずですので、何が出力されるか見てみましょう:
$ python main.py 1
Traceback (most recent call last):
File "C:\Projects\Python\TinderBolt\main.py", line 19, in <module>
bad()
File "C:\Projects\Python\TinderBolt\main.py", line 17, in bad
print(e)
^
UnboundLocalError: cannot access local variable 'e' where it is not associated with a value
ここでいったい何が起こったのでしょうか?問題は、Pythonでは 例外ブロック内のオブジェクトがその外では利用できない ということです。(理由は、そうでないとそのブロック内のオブジェクトがガベージコレクタが起動してそれらへの参照を削除するまでメモリに残ることになるからです)。
この問題を回避する方法の1つは、例外ブロック外でそのオブジェクトへの参照を保持して、それが引き続き利用可能であるようにすることです。前述の例をこの方法を使って修正し、動作するコードにしてみましょう:
import sys
def bar(i):
if i == 1:
raise KeyError(1)
if i == 2:
raise ValueError(2)
def good():
exception = None
try:
bar(int("1"))
except KeyError as e:
exception = e
print('key error')
except ValueError as e:
exception = e
print('value error')
print(exception)
good()
9.4 __del__
メソッドの誤った使用
インタプリタがオブジェクトを削除すると、最初にそのオブジェクトに __del__
関数があるかどうかを確認し、あれば、そのオブジェクトを削除する前にその関数を呼び出します。これは、オブジェクトが外部リソースやキャッシュをきれいにするために使いたいときに非常に便利です。
次のような mod.py
ファイルがあるとしましょう:
import foo
class Bar(object):
...
def __del__(self):
foo.cleanup(self.myhandle)
そして、別のファイル another_mod.py
で次のようなことをしようとすると:
import mod
mybar = mod.Bar()
そして、ひどい AttributeError
に遭遇します。
なぜでしょうか?というのも、ここに 記載されているように、インタプリタが終了するときには、すべてのモジュールのグローバル変数が None
になります。その結果、上記の例では、__del__
の呼び出し時には、foo
の名前はすでに None
に設定されています。
この「スター付きの問題」の解決策は、特別な atexit.register()
メソッドを使うことです。こうすることで、プログラムが通常の終了時にあなたの handle
がインタプリタが終了する前に削除されるようになります。
このような修正を加えた mod.py
のコードは次のようになるでしょう:
import foo
import atexit
def cleanup(handle):
foo.cleanup(handle)
class Bar(object):
def __init__(self):
...
atexit.register(cleanup, self.myhandle)
このような実装は、プログラムの通常の終了後に必要なクリーンアップを呼び出すシンプルで信頼性のある方法を提供します。もちろん、self.myhandle
と関連付けられたオブジェクトをどう扱うかの決定は foo.cleanup
に委ねられていますが、考え方はお分かりいただけたかと思います。
GO TO FULL VERSION