【Python】循環importをほどく:共通部分の切り出しで依存関係を整える

PythonのTopに戻る


a.pyがb.pyを読み込み、b.pyがa.pyの定義を必要とする循環importでは、まだ定義されていない名前へアクセスして失敗することがある。共通の関数や定数を第三のモジュールへ切り出し、読み込みの向きをそろえると解消しやすい。関数の中へimportを移す応急処置だけで済ませず、どの部品が何を知る必要があるかを見直してみよう。

読み込み途中のモジュールを参照している

Pythonはモジュールの初期化を上から進める。aを読み込んでいる途中でbを読み、bからaの関数を取り出そうとすると、その関数のdef文まで到達していないことがある。ファイルに定義が書いてあるのに、実行時にはまだ存在しないという状態になる。

循環があれば必ずその場でエラーになるわけではない。名前を取り出す時点や起動順によって、たまたま動く場合もある。そのためimportの行を並べ替えるだけでは、別の入口から読み込んだときに再び壊れることがある。

二つのファイルで失敗を再現する

以下の二つを同じフォルダーへ置いてimport aすると、aはcleanを定義する前にbを読み込む。bはそのcleanをすぐに必要とするので、初期化の途中で失敗する。ここは意図的な失敗例である。

失敗例:a.py

from b import label


def clean(text):
    return text.strip().lower()

失敗例:b.py

from a import clean


def label(text):
    return "sample:" + clean(text)

エラー文中のpartially initializedという表現は、定義が完全に読み込まれる前に参照された可能性を示す。名前の衝突でも似た状態になる場合があるため、トレースバックのファイル配置も合わせて確かめる。

共有定義を独立したモジュールへ出す

cleanは表示機能から独立した文字列処理であり、aだけが所有する必要はない。そこでcommon.pyへ移し、aとbがそれぞれ必要な部品を読み込む。共通モジュールはaやbを読み込まず、依存が一方向になるようにする。

修正後:common.py

def clean(text):
    return text.strip().lower()

修正後:b.py

from common import clean


def label(text):
    return "sample:" + clean(text)

修正後:a.py

from b import label


def main():
    print(label(" A "))


if __name__ == "__main__":
    main()

実行結果

Before: ImportError
After: sample:a

失敗例の結果は、実際に発生した例外クラスだけを表示した。修正後はaとbのどちらを先にimportしても、表示用の関数を呼び出せる。読み込み順に依存しないところまで確認しておくと、別のプログラムから再利用しやすい。

切り出す内容を小さく保つ

共通モジュールへ何でも集めると、今度はそこから多くの処理を読み込むようになり、循環が復活しやすい。型、定数、変換関数など、上位の処理を知らなくても成立する定義を中心に置く。ファイルを分けることそのものではなく、役割を分けることが目的である。

相互に呼び出す処理が本当に必要な場合は、必要な関数を引数へ渡す方法も考えられる。部品Aが部品Bをimportして探す代わりに、入口が両方を用意して組み合わせる。この形なら、入口だけが全体の関係を知り、部品同士の依存を減らせる。

関数内importを使う場合の条件

importを関数内へ置くと、呼び出し時まで読み込みを遅らせられる。そのため初期化中の循環を避けられる場合はあるが、依存関係そのものが消えるわけではない。呼び出し経路によって再び途中のモジュールへ触れるなら、問題は残る。

任意依存の読み込みを必要なときだけ行う場合など、遅らせることに意味がある用途もある。循環が見つかったときには、まず共有定義の移動や、呼び出し方向の整理で解けないかを検討し、そのうえで遅延読み込みが必要な理由を残すとよい。

修正後の確認は、すでにimport済みの対話環境だけで行わない。新しいPythonプロセスで各入口を読み込み、必要な関数を一度呼んでみる。sys.modulesに残っている状態が影響すると、初回起動だけの失敗を見落とすことがある。

動作確認と参考資料

掲載例はLinux・CPython 3.12.14で動作確認した。OS固有のコマンドや環境ごとに変わるパスは、本文中の条件を確認して使ってほしい。

関連するTips


PythonのTopに戻る