Qiitaで検索しても手早くcircular importを解決する記事が見当たらず、初心者に不親切そうだと見受けられたので、手早く解説する記事をここに置きます。
Circular Importとは?
日本語では 循環参照 というキーワードに当たります。
これで検索するといっぱい日本語の情報にヒットするでしょう。
今回のバグを言葉で表すと、 モジュールが巡り巡って自分をimport
するとエラーになる ことです。
単純な例(2モジュール)
from B import f_b
def f_a():
f_b()
from A import f_a
def f_b():
f_a()
これは明確にA→B
とB→A
の依存関係がありますね。
慣例通り、X→Y
は「XがYに依存している」という意味で用いています。
例えばA.py
を実行したり、他のモジュールからimport A
したりすると、circular import
エラーがでます。
これは、A.py
の
from B import f_b
の時点で、B.py
のところへ飛び、
from A import f_a
が実行され、モジュールAの中でAがimport
されることになります。
お気づきの通り、そうするとAの中でBがimport
され、またBがimport
され...の無限ループになるので、Python側で自モジュールをimport
された時点でストップしてくれているわけです。
巡り巡る例(3モジュール)
from C import f_c
def f_a():
f_c()
from A import f_a
def f_b():
f_a()
from B import f_b
def f_c():
f_b()
このような場合も、 循環参照であり、circular import
となります。
解決方法
型ヒント目的で循環している場合
もし型ヒントのためだけに記述したimport
で悩まされている場合は、次の行き先はこちらです:PythonにおいてannotationsとTYPE_CHECKINGで循環参照を防ぐ
まあ、型ヒントを使っている時点でデータ的には循環依存関係にあるので、設計的にあまりよくはないと一般的にいわれていますが、それが最適構造である場合も多いので、なんともいえないですね。
一般的に循環参照はシステムに危害を加えない限り必ずしも無くせねばならないというものではないです。一方なくせばなくすほど良いですけどね。
機能上循環している場合
つまるところ、 循環参照をなくしてください
しかしこれはけっこう難しいモジュール・クラス設計の命題なので、一般的に言われているプラクティスをいくつか紹介します。
もっと知りたい方は「循環参照」で検索。
他モジュールのクラスのインスタンスを受け取る、インスタンスのメソッドを呼ぶ場合は、循環参照ではありますが、circular import
エラーは出ません。
Python
の仕様によりimport
せずとも受け取ったり呼べたりするからです。動的型付けの強み。(型ヒントのためimport
する場合は上述参照)
それでもアーキテクチャ的に気になる場合は、他言語の「インターフェース追加による循環参照解決」の議論を調べてみてください。Python
にはinterface
はないものの多重継承ができるので、インターフェースを通常のクラスと読み替えることで同じことができます。
依存関係を一方通行にする
最も単純でありながら、一番アーキテクチャを綺麗にする選択です。
ここらへんは オブジェクト指向設計 を学ぶとよいと思われます。
もし A has B
の関係なら、A→B
に依存関係を限定する、など。
同じモジュール(ファイル)に結合する
from B import f_b
def f_a():
f_b()
from A import f_a
def f_b():
f_a()
の場合、
def f_a():
f_b()
def f_b():
f_a()
と書けばエラーは出ないです。
循環参照している時点で密結合している可能性が高いので、こうやってくっつけちゃうのもアリだと思います。
もちろんモジュール全てを結合しなくとも、その中で問題になっている関数などだけ引っ越しさせる、という選択肢もありますね。
モジュールから分離する
結合の次は分離です。 データの管理にクラス( データクラス )を用いているときなどに有効だと思います。
例えば「データ分析」モジュールAnalyzer.py
と「グラフ表示」モジュールVisualizer.py
があったとして、
from Visualizer import visualize
def analyze:
#分析する
pass
#可視化する
visualize(data)
class ResultAnalyzed:
#分析結果のデータ
from Analyzer import ResultAnalyzed
def visualize(data: ResultAnalyzed):
# 可視化する
だと、2者で循環参照していますね。
ここでデータクラスResultAnalyzed
を分離すると、
from Visualizer import visualize
+from ResultAnalyzed import ResultAnalyzed
def analyze:
#分析する
pass
#可視化する
visualize(data)
-class ResultAnalyzed:
- #分析結果のデータ
+class ResultAnalyzed:
+ #分析結果のデータ
+from Analyzer import ResultAnalyzed
def visualize(data: ResultAnalyzed):
# 可視化する
もっと言えば、システム進行をAnalyzer.py
ではなく、新しくManager.py
システムを作成して、
from Visualizer import analyze
from Visualizer import visualize
#進行
result = analyze(data)
visualize(result)
とでも書けば、Visualizer
の独立性が高まり、アーキテクチャが綺麗になります。
システムの順番管理もしやすくなるのでいいですね。
「まとめ役」としてのManager
という名前はUnityゲーム開発の慣習なのですが、それぞれの分野での慣習的な名称が異なるかもしれません。
さいごに
いいね頂けると嬉しいです><