LoginSignup
2
2

Qiitaで検索しても手早くcircular importを解決する記事が見当たらず、初心者に不親切そうだと見受けられたので、手早く解説する記事をここに置きます。

Circular Importとは?

日本語では 循環参照 というキーワードに当たります。
これで検索するといっぱい日本語の情報にヒットするでしょう。

今回のバグを言葉で表すと、 モジュールが巡り巡って自分をimportするとエラーになる ことです。

単純な例(2モジュール)

A.py
from B import f_b

def f_a():
    f_b()
B.py
from A import f_a

def f_b():
    f_a()

これは明確にA→BB→Aの依存関係がありますね。

慣例通り、X→Yは「XがYに依存している」という意味で用いています。

例えばA.pyを実行したり、他のモジュールからimport Aしたりすると、circular importエラーがでます。

これは、A.py

A.py
from B import f_b

の時点で、B.pyのところへ飛び、

B.py
from A import f_a

が実行され、モジュールAの中でAがimportされることになります。

お気づきの通り、そうするとAの中でBがimportされ、またBがimportされ...の無限ループになるので、Python側で自モジュールをimportされた時点でストップしてくれているわけです。

巡り巡る例(3モジュール)

A.py
from C import f_c

def f_a():
    f_c()
B.py
from A import f_a

def f_b():
    f_a()
C.py
from B import f_b

def f_c():
    f_b()

このような感じで依存関係が循環参照していますね。
image.png

このような場合も、 循環参照であり、circular importとなります。

解決方法

型ヒント目的で循環している場合

もし型ヒントのためだけに記述したimportで悩まされている場合は、次の行き先はこちらです:PythonにおいてannotationsとTYPE_CHECKINGで循環参照を防ぐ

まあ、型ヒントを使っている時点でデータ的には循環依存関係にあるので、設計的にあまりよくはないと一般的にいわれていますが、それが最適構造である場合も多いので、なんともいえないですね。
一般的に循環参照はシステムに危害を加えない限り必ずしも無くせねばならないというものではないです。一方なくせばなくすほど良いですけどね。

機能上循環している場合

つまるところ、 循環参照をなくしてください
しかしこれはけっこう難しいモジュール・クラス設計の命題なので、一般的に言われているプラクティスをいくつか紹介します。
もっと知りたい方は「循環参照」で検索。

他モジュールのクラスのインスタンスを受け取る、インスタンスのメソッドを呼ぶ場合は、循環参照ではありますが、circular importエラーは出ません。
Pythonの仕様によりimportせずとも受け取ったり呼べたりするからです。動的型付けの強み。(型ヒントのためimportする場合は上述参照)

それでもアーキテクチャ的に気になる場合は、他言語の「インターフェース追加による循環参照解決」の議論を調べてみてください。Pythonにはinterfaceはないものの多重継承ができるので、インターフェースを通常のクラスと読み替えることで同じことができます。

依存関係を一方通行にする

最も単純でありながら、一番アーキテクチャを綺麗にする選択です。

ここらへんは オブジェクト指向設計 を学ぶとよいと思われます。
もし A has Bの関係なら、A→B に依存関係を限定する、など。

同じモジュール(ファイル)に結合する

A.py
from B import f_b

def f_a():
    f_b()
B.py
from A import f_a

def f_b():
    f_a()

の場合、

AB.py
def f_a():
    f_b()

def f_b():
    f_a()

と書けばエラーは出ないです。
循環参照している時点で密結合している可能性が高いので、こうやってくっつけちゃうのもアリだと思います。

もちろんモジュール全てを結合しなくとも、その中で問題になっている関数などだけ引っ越しさせる、という選択肢もありますね。

モジュールから分離する

結合の次は分離です。 データの管理にクラス( データクラス )を用いているときなどに有効だと思います。

例えば「データ分析」モジュールAnalyzer.pyと「グラフ表示」モジュールVisualizer.pyがあったとして、

Analyzer.py
from Visualizer import visualize

def analyze:
    #分析する
    pass

    #可視化する
    visualize(data)

class ResultAnalyzed:
    #分析結果のデータ
Visualizer.py
from Analyzer import ResultAnalyzed

def visualize(data: ResultAnalyzed):
    # 可視化する

だと、2者で循環参照していますね。


ここでデータクラスResultAnalyzedを分離すると、

Analyzer.py
from Visualizer import visualize
+from ResultAnalyzed import ResultAnalyzed

def analyze:
    #分析する
    pass

    #可視化する
    visualize(data)

-class ResultAnalyzed:
-    #分析結果のデータ
ResultAnalyzed.py
+class ResultAnalyzed:
+    #分析結果のデータ
Visualizer.py
+from Analyzer import ResultAnalyzed

def visualize(data: ResultAnalyzed):
    # 可視化する

とすれば、
image.png
循環がなくなります。

もっと言えば、システム進行をAnalyzer.pyではなく、新しくManager.pyシステムを作成して、

Manager.py
from Visualizer import analyze
from Visualizer import visualize

#進行
result = analyze(data)
visualize(result)

とでも書けば、Visualizerの独立性が高まり、アーキテクチャが綺麗になります。
システムの順番管理もしやすくなるのでいいですね。

「まとめ役」としてのManagerという名前はUnityゲーム開発の慣習なのですが、それぞれの分野での慣習的な名称が異なるかもしれません。

さいごに

いいね頂けると嬉しいです><

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2