循環インポートとcannot import nameの謎

Flaskのバックエンドを書いている途中にimportのエラーで不思議に思った点があるので検証を行いました。

問題のコード

以下のようなディレクトリ構造で、それぞれのファイルには次のような内容が書かれています。

意図した動作としては、main.pyを実行するとmain.pyからpkg1パッケージ内のmod1モジュールの関数func2をロードしてmain.py内で実行します。

mod1モジュールの関数func2main.pyにある関数func1の返り値をprintする関数です。

mytest
|-- pkg1/ (パッケージ1)
   |-- __init__.py
   |-- mod1.py (モジュール1)
|-- main.py (実行ファイル)
# main.py
from pkg1.mod1 import func2

var1 = 'foo'


def func1():
    return var1

if __name__ == '__main__':
    func2()
# pkg1/__init__.py
# pkg1/mod1.py
from main import func1

def func2():
    print(func1())

インポート時にエラーが起きる

main.pypkg1/mod1.pyで相互にインポートしているので、実行時に次のようなエラーが発生します。

# (bash)
$ python main.py
Traceback (most recent call last):
  File "main.py", line 2, in <module>
    from pkg1.mod1 import func2
  File "...\mytest\pkg1\mod1.py", line 2, in <module>
    from main import func1
  File "...\mytest\main.py", line 2, in <module>
    from pkg1.mod1 import func2
ImportError: cannot import name 'func2' from 'pkg1.mod1' (...\mytest\pkg1\mod1.py)

解決策

main.pyのみを編集する解決策としては次の2通りが有ります。

解決策1 : ローカルスコープインポート

1つ目の解決策は、循環インポートを応急的に解消する方法です。

# main.py
var1 = 'foo'


def func1():
    return var1

if __name__ == '__main__':
    from pkg1.mod1 import func2
    func2()

main.pyでのモジュールの参照を、main.pyがトップレベルとして実行された時に限定します。

これで、mod1.pymain.pyを参照した時にはmod1.pyが参照されないため、循環インポートが解消されて正常に実行出来るようになります。

解決策2 : ワイルドカードインポート

2つ目の解決策は、func2を名指しでインポートしていた所をワイルドカードインポートに変更します。

# main.py
from pkg1.mod1 import *

var1 = 'foo'


def func1():
    return var1

if __name__ == '__main__':
    func2()

こうすると、プログラムが正常に実行出来るようになります。

ちょっと待って

1つ目の解決策は循環インポートが解消されたので実行出来るようになるのは理解出来ます。

しかし、2つ目の解決策は何も解決しているように見えません。

検証

どういう経路で実行されているのか知るために、main.pypkg1/mod1.pyのファイルの先頭とインポートの前後にprint()を置きました。

# ファイルの先頭
print('Loading ファイル名 =', __name__)
# インポートの前後
print('Before import, pkg1/mod1.py =', __name__)
# from ... import ...
print('After import, pkg1/mod1.py =', __name__)

また、var1が定義された時間を調べるためにtimeモジュールを使います。

# var1 = 'foo'
var1 = time.time()
print(__name__, var1)

解決策1の経路

解決策1の場合の経路です。

# (bash)
$ python main.py
Loading main.py = __main__              # 実行コマンドによるロード
__main__ 1580243970.7842584             # __main__でvar1が定義される
Before import, main.py = __main__       # main => pkg1/mod1 開始
Loading pkg1/mod1.py = pkg1.mod1        # mainからのimportによるロード
Before import, pkg1/mod1.py = pkg1.mod1 # pkg1/mod1 => main 開始
Loading main.py = main                  # pkg1/mod1からののimportによるロード
main 1580243970.7892146                 # mainでvar1が定義される
After import, pkg1/mod1.py = pkg1.mod1  # pkg1/mod1 => main 終了
After import, main.py = __main__        # main => pkg1/mod1 終了
1580243970.7892146                      # mainでのvar1の結果が呼ばれている

ローカルスコープインポートのため、main.pyからpkg1/mod1.pyへのインポートは1回しか発生していません。

また、実行コマンドで一度main.pyは実行されていますが、pkg1/mod1.pyからのインポート時にもう一度実行してその結果をインポートしているのが、最後の結果から分かります。

解決策2の経路

解決策2の場合の経路です。

# (bash)
$ python main.py
Loading main.py = __main__              # 実行コマンドによるロード
Before import, main.py = __main__       # main => pkg1/mod1 開始
Loading pkg1/mod1.py = pkg1.mod1        # mainからのimportによるロード
Before import, pkg1/mod1.py = pkg1.mod1 # pkg1/mod1 => main 開始
Loading main.py = main                  # pkg1/mod1からのimportによるロード
Before import, main.py = main           # main => pkg1/mod1 開始
# pkg1/mod1.pyが読み込まれたらLoading ...が表示されるはず
After import, main.py = main            # main => pkg1/mod1 終了
main 1580244277.4291725                 # mainでvar1が定義される
After import, pkg1/mod1.py = pkg1.mod1  # pkg1/mod1 => main 終了
After import, main.py = __main__        # main => pkg1/mod1 終了
__main__ 1580244277.4301698             # __main__でvar1が定義される
1580244277.4291725                      # mainでのvar1の結果が呼ばれている

ローカルスコープインポートと違い、コードが実行される度にインポート自体は行われているようです。

但し、解決策2ではインポートガードが機能しているので、pkg1/mod1.pyの2回目の実行は阻止されています。

エラーの場合の経路

最初に示したコードの場合の経路です。

# (bash)
$ python main.py
Loading main.py = __main__              # 実行コマンドによるロード
Before import, main.py = __main__       # main => pkg1/mod1 開始
Loading pkg1/mod1.py = pkg1.mod1        # mainからのimportによるロード
Before import, pkg1/mod1.py = pkg1.mod1 # pkg1/mod1 => main 開始
Loading main.py = main                  # pkg1/mod1からのimportによるロード
Before import, main.py = main           # main => pkg1/mod1 開始
Traceback (most recent call last):      # インポートエラー発生
  ...
ImportError: cannot import name 'func2' from 'pkg1.mod1' (...\mytest\pkg1\mod1.py)

pkg1/mod1.pyの2回目の実行の前までは解決策2と同じ経路のように見えます。

考察

解決策1と解決策2では、インポートの位置による実行経路の違いが見られ、どちらもpkg1/mod1.pyのインポート時に実行された結果を利用するようでした。

しかし、解決策2の場合ではインポートガードによってpkg1/mod1.pyの2回目の実行が回避されているのに対し、エラーの場合ではそのタイミングでエラーが発生しています。

つまり、名指しでインポートを行うと、インポート名を明示的に指定している関係でインポートガードが行えないのではないかと考えられます。

まとめ

循環インポートはコードが無駄に実行されてしまったり今回のようなちょっとしたエラーにもなるのでそもそも循環インポートになるような構造は避けるべきですが、仮に循環インポートになってしまったとしてもPythonの賢いインポートガードによって上手にループが回避されているのが分かりました。

ワイルドカードインポートはPEP 8で非推奨になっているし名前空間が分かりづらくなるので、どうしても循環構造が生まれてしまう場合は解決策1の方が望ましいかも知れません。勿論、ファイルの途中でインポートするのもPEP 8では推奨されていないので、from pkg1 import mod1でモジュールレベルインポートした後、関数オブジェクトのようにfunc2 = mod1.func2で別名を付けるのが最適解だと思います。

ワイルドカードを使った import (from import *) は避けるべきです。(pep8-ja/import)

ちなみに、このエラーはFlask-SQLAlchemyでビューの作成をblueprintでしようとしていてこのような構造になったので発生しました。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です