Pythonでモジュールを分割して開発していると、「循環import(循環インポート)」という問題に遭遇することがあります。
これは、2つ以上のモジュールが互いをインポートし合うことで発生します。
基本的なエラー例
まず、典型的なエラーが発生するケースを見てみましょう。
# main.py
from sub import print_add
def add(a: int, b: int) -> int:
return a + b
if __name__ == "__main__":
print_add(1, 2)
# sub.py
from main import add
def print_add(a: int, b: int) -> None:
print(add(a, b))
この状態で python3 main.py を実行すると、ImportError が発生します。
エラーの原因
-
main.pyがsubモジュールからprint_addをインポートしようとする - Pythonが
sub.pyの読み込みを開始する -
sub.pyがmainモジュールからaddをインポートしようとする - ここで循環が発生。Pythonは再び
main.pyを読み込もうとするが、main.pyはsub.pyの完了待ちのため、まだ完全に初期化されていない - 結果として、
sub.pyは未完成のmainモジュールからaddを見つけられず、ImportErrorが発生する
一般的な回避策
関数内でインポートを行うことで、循環のタイミングをずらす方法があります。
# sub.py
def print_add(a: int, b: int) -> None:
from main import add
print(add(a, b))
発展例:これらのパターンはどう動作する?
次に、import と from ... import の違いによって挙動が変わる例を見てみましょう。
一見どれも循環インポートに見えますが、実際には挙動が異なります。
パターン1
# main.py
from sub import print_add
def add(a: int, b: int) -> int:
return a + b
if __name__ == "__main__":
print_add(1, 2)
# sub.py
import main
def print_add(a: int, b: int) -> None:
print(main.add(a, b))
結果: ❌ ImportError 発生
パターン2
# main.py
import sub
def add(a: int, b: int) -> int:
return a + b
if __name__ == "__main__":
sub.print_add(1, 2)
# sub.py
from main import add
def print_add(a: int, b: int) -> None:
print(add(a, b))
結果: ✅ 正常に実行される
パターン3
# main.py
import sub
def add(a: int, b: int) -> int:
return a + b
if __name__ == "__main__":
sub.print_add(1, 2)
# sub.py
import main
def print_add(a: int, b: int) -> None:
print(main.add(a, b))
結果: ✅ 正常に実行される
パターン4
# main.py
import sub
def add(a: int, b: int) -> int:
return a + b
sub.print_add(1, 2)
# sub.py
import main
def print_add(a: int, b: int) -> None:
print(main.add(a, b))
結果: ❌ AttributeError 発生
なぜこうなるのか?:詳細解説
これらの違いは、Pythonの「モジュール初期化のタイミング」と「名前解決の仕組み」に起因します。
成功と失敗の分岐点
-
import <モジュール>の場合(例:パターン2のmain.py)
import subにより、subというモジュールオブジェクトへの参照が確保されます。
この時点ではsubの中身(関数など)は未定義でも、オブジェクト自体は存在します。
実際にsub.print_add()が呼ばれるのはmainの初期化完了後なので、問題なく動作します。 -
from <モジュール> import <名前>の場合(例:パターン1のmain.py)
from sub import print_addの実行時点でsub.print_addを直接参照する必要があるため、
subの初期化が終わっていないとImportErrorになります。
パターン4で AttributeError になる理由
if __name__ == "__main__": がないため、モジュール初期化の途中で sub.print_add(1, 2) が実行されてしまいます。
-
main.pyがimport subを実行し、sub.pyの読み込みを開始 -
sub.pyがimport mainを実行し、再びmainの初期化を試みる - この時点で
subは空のモジュールオブジェクトとして登録済み -
add()が定義される - 直後に
sub.print_add(1, 2)が呼ばれるが、sub内の関数はまだ定義途中 - よって
AttributeErrorが発生する
まとめ
- 循環インポートのエラーは、モジュールの初期化が終わる前に未定義の属性へアクセスすることで起きる
-
import ...はモジュール全体を遅延的に参照できるため、エラーを回避できる場合がある -
from ... import ...は属性を即時に解決するため、循環関係では失敗しやすい -
if __name__ == "__main__":の利用は、循環インポートによる実行タイミング問題の回避にも有効
循環インポートを根本的に避けるには、モジュールの依存関係を整理し、共通部分を別モジュールへ切り出す設計が推奨されます。