循環import問題というものがあります。
例えば、
# coding: utf-8
import piyo
class Base:
pass
class Hoge(Base):
pass
と、
# coding: utf-8
import hoge
class Piyo(hoge.Base):
pass
という2つのPythonスクリプトがあったとして、「python3 hoge.py
」と実行するとエラーは起きないのですが、「python3 piyo.py
」と実行すると、
Traceback (most recent call last):
File "piyo.py", line 3, in <module>
import hoge
File "hoge.py", line 3, in <module>
import piyo
File "piyo.py", line 5, in <module>
class Piyo(hoge.Base):
AttributeError: partially initialized module 'hoge' has no attribute 'Base' (most likely due to a circular import)
というようにAttributeErrorが発生してしまいます。
(今見ると、ご丁寧に「most likely due to a circular import」と言ってくれてるんですね)
これに対して、Base
クラスは別のスクリプトにし、hoge.py
/piyo.py
の双方からそのスクリプトをimportする、というのが解決法の一つになります。
はてさて、時折このような問題が出るとQ&Aサイトで出てくるのですが、回答は先のようなものでいいのですが、実は例外が発生する原因は結構深い話で、いつも「んー?」と悩んで、理由がわかると「あ、これ、前にも悩んだやつですわ。またかい」と思っています。
今日は、その事をメモっておこうかと思います。
さて、この問題の原因を理解するために、知っておかなければならないPythonの仕様を挙げておきます。
-
import
文は、そのモジュールのスクリプトを実行する。 - モジュールAの中でモジュールBをimportされた場合、モジュールAのそれ以降の処理はモジュールBの処理が終わってから実行される。
- importされたモジュールオブジェクトは、
sys.modules
にモジュール名をキーとして保存され、2回目以降にimportされた場合は、sys.modules
に保存されたオブジェクトを返すだけで、モジュールのスクリプトの処理は実行されることはない。
さて、これを踏まえて、先の「python3 piyo.py
」を実行した際に何が起きるか、検証してみましょう。
- 「
piyo.py
」の中で、「import hoge
」とhoge.py
がimportされます。この時点で、Piyo
クラスはまだ定義されていません。 - importされた「
hoge.py
」の中で、「import piyo
」が実行されますが、「piyo.py
」は既にロードされているので、何もしません。 - 「
hoge.py
」のそれ以降に書かれている、Base
クラス、Hoge
クラスが定義されます。
このように、何の問題もなく実行され…あれ?
なんか違います。
今度は、「python3 hoge.py
」を実行してみましょう。
- 「
hoge.py
」の中で、「import piyo
」とpiyo.py
がimportされます。この時点でBase
クラスとHoge
クラスは定義されていません。 - importされた「
piyo.py
」の中で、「import hoge
」が実行されますが、「hoge.py
」は既にロードされているので、何もしません。 -
Piyo
クラスの派生元クラスとして、hoge.Base
クラスを参照しますが、hoge.py
の中でBase
クラスの定義が実行されていないので、hoge
モジュールの属性にBase
というものはなく、AttributeError
例外が発生します。
このように、エラーが発生して…あれ?
このように、原因を突き止めると実際の動作と逆のようになります。
いつもここで一旦悩むのですが、もう一つPythonの仕様を思い出して解決します。
- 最初に起動するスクリプトは、「
__main__
」というモジュール名になる。
これを踏まえて、もう一度「python3 piyo.py
」の動作を検証してみましょう。
- 「
piyo.py
」の中で、「import hoge
」とhoge.py
がimportされます。この時点で、Piyo
クラスはまだ定義されていません。 - importされた「
hoge.py
」の中で、「import piyo
」が実行されます。「piyo.py
」は既にロードされていますがそれはモジュール名が「__main__
」というものであり、「piyo
」というモジュールはロードされていないため、再度「piyo.py
」が実行されます。この時点で、この後に書かれているBase
クラスとHoge
クラスは実行されていないので、まだ定義されていません。 - importされた「
piyo.py
」の中で、「import hoge
」が実行されますが、「hoge.py
」は既にロードされているので、何もしません。 -
Piyo
クラスの派生元クラスとして、hoge.Base
クラスを参照しますが、hoge.py
の中でBase
クラスの定義が実行されていないので、hoge
モジュールの属性にBase
というものはなく、AttributeError
例外が発生します。
確かに、AttributeError
例外が発生しますね。
試しに、
# coding: utf-8
import hoge
import sys
print('\n'.join([repr(n) for n in sys.modules.items()]))
class Piyo(hoge.Base):
pass
と、sys.modules
の中身を見てみましょう。
('__main__', <module '__main__' from 'a/piyo.py'>)
('piyo', <module 'piyo' from '/path/to/a/piyo.py'>)
(必要なものを除き、省略)
上記のように、「piyo.py
」が二度読み込まれているのが解ると思います。
さて、おさらいです。
基本的に、スクリプトは一回importされたら二回実行される事はないのですが、起動時に実行するスクリプトは二回実行される事があるという事です。
ですので、起動時に実行されるスクリプトは、
if __name__ == '__main__':
main()
というように、自身のモジュール名を見て、最初の一回のみ実行するようにすべき、ということですね。