【Python】circular importエラーをシンプルに解説

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

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

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

### 単純な例（2モジュール）
```python:A.py
from B import f_b

def f_a():
    f_b()
```

```python:B.py
from A import f_a

def f_b():
    f_a()
```

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

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

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

これは、`A.py`の
```python:A.py
from B import f_b
```
の時点で、`B.py`のところへ飛び、
```python:B.py
from A import f_a
```
が実行され、モジュールAの中でAが`import`されることになります。

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

### 巡り巡る例（3モジュール）
```python:A.py
from C import f_c

def f_a():
    f_c()
```

```python:B.py
from A import f_a

def f_b():
    f_a()
```

```python:C.py
from B import f_b

def f_c():
    f_b()
```

このような感じで依存関係が循環参照していますね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/b9810636-1bf2-b69d-7eff-057f63b79122.png)

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

## 解決方法
### 型ヒント目的で循環している場合
もし型ヒントのためだけに記述した`import`で悩まされている場合は、次の行き先はこちらです：[PythonにおいてannotationsとTYPE_CHECKINGで循環参照を防ぐ](https://zenn.dev/ganariya/articles/python-lazy-annotation)

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

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

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

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

#### 依存関係を一方通行にする
最も単純でありながら、一番アーキテクチャを綺麗にする選択です。

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

#### 同じモジュール（ファイル）に結合する
```python:A.py
from B import f_b

def f_a():
    f_b()
```

```python:B.py
from A import f_a

def f_b():
    f_a()
```
の場合、

```python: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`を分離すると、
```diff_python: Analyzer.py
from Visualizer import visualize
+from ResultAnalyzed import ResultAnalyzed

def analyze:
    #分析する
    pass

    #可視化する
    visualize(data)

-class ResultAnalyzed:
-    #分析結果のデータ
```

```diff_python: ResultAnalyzed.py
+class ResultAnalyzed:
+    #分析結果のデータ
```

```diff_python:Visualizer.py
+from Analyzer import ResultAnalyzed

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

とすれば、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/2d7c7020-0c73-d03d-ffe9-31db09d4d0e3.png)
循環がなくなります。

もっと言えば、システム進行を`Analyzer.py`ではなく、新しく`Manager.py`システムを作成して、
```python: Manager.py
from Visualizer import analyze
from Visualizer import visualize

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

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

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

## さいごに
いいね頂けると嬉しいです><