【Python】モジュール構成規則を考えてみる

## はじめに
`Python`のモジュール設計、難しいですよね。

* 何を`__init__.py`でまとめるのか（まとめないと、`import`文がすごく多くなる）
* どのようにモジュールの階層を形成するのか
* `circular import`は避けなければならない

という中で、モジュールを上手にディレクトリに配置しなければなりません。
モジュール設計の指針も有名なものはなさそうなので（あればゴメンナサイ）、自分が考えた設計規則を紹介してみたいと思います。

## 紹介
:::note
そのまま使い回せる英語表記`markdown`：[GitHubレポジトリへ飛ぶ](https://github.com/konbraphat51/module_rule/blob/main/RULE.md)
:::

### 条文（和訳）
詳細は後述です。

1. 基本的に下位モジュールに依存してください。メインモジュールをディレクトリ名と同じにして、メインモジュールの下位モジュールはそのディレクトリに入れてください。

1. モジュールAの下位モジュールがある場合は、それらを全て（モジュールAも下位モジュールも）パッケージAを作成して格納し、単一のモジュールとして使えるように`__init__.py`で隠蔽してください。

1. 同位モジュールに依存するときは、依存関係UMLを作成/更新してください

1. 他のパッケージにあるモジュールに依存する必要のある場合は、共通最下位ディレクトリの依存関係UMLを更新してください。

1. 同位モジュールを`import`する場合は、モジュール名で`import`してください。
    下位モジュールを`import`する場合は、パッケージ名で`import`してください。

1. 他最上位パッケージを`import`するときは、その最上位パッケージ名で`import`してください
    同最上位パッケージを`import`するときは、モジュール名で`import`してください

### 詳細（和訳）
#### 1. 基本的に下位モジュールに依存してください。メインモジュールをディレクトリ名と同じにして、メインモジュールの下位モジュールはそのディレクトリに入れてください。

![image](https://github.com/konbraphat51/module_rule/assets/101827492/60e9ca70-6e8c-4e29-9c35-455a7dfcea85)

この場合、

* `ABA`パッケージのメインモジュールは`ABA.py`であり、
* `ABA.py`の下位モジュールは、同じディレクトリにある`ABAA.py`と`ABAB.py`です。

`ABA.py`は上位モジュールである`AB.py`や`AA.py`からではなく、
下位モジュールである`ABAA.py`または`ABAB.py`から`import`すべきです。

#### 2. モジュールAの下位モジュールがある場合は、それらを全て（モジュールAも下位モジュールも）パッケージAを作成して格納し、単一のモジュールとして使えるように`__init__.py`で隠蔽してください。

**lowest-common（最も低い共通）** とは次の意味です：

`A->B` の依存関係がある場合、`A` と `B` の最も低いレベルのディレクトリが存在します。

`A_A_A` が `A_B_B` に依存する場合、以下のようなケースでは：

![Image](https://user-images.githubusercontent.com/101827492/285468378-8136fd12-3065-419e-93cc-8a2729a641b8.png)

**lowest-common package（共通最下位パッケージ）** は `A_directory` です。

**lowest-common package** の依存関係のUMLを描いてください

#### 3. 同位モジュールに依存するときは、依存関係UMLを作成/更新してください

![image](https://github.com/konbraphat51/module_rule/assets/101827492/5c06ed97-eba3-41af-82cd-d350e5e221c2)

こちらの場合、

`ABBA.py` と `ABBB.py` は**同位の**モジュールです。

`ABBB.py` には以下のような記述があります：

```python
from PythonRoot.A.AB.ABB.ABBA import abba
```

これは `ABBB.py` が `ABBA.py` に依存していることを意味します。

もしパッケージ内で、サブ→メインの依存関係以外に他の依存関係がある場合、パッケージの依存関係のUMLを描く必要があります。

この場合、`ABB` パッケージの依存関係のUMLは以下のようになります：

![image](https://github.com/konbraphat51/module_rule/assets/101827492/43a23278-f0d9-4406-a4e0-85f5e09cd692)

#### 4. 他のパッケージにあるモジュールに依存する必要のある場合は、共通最下位ディレクトリの依存関係UMLを更新してください。

もしパッケージ `A` が以下のような構造であるとします：

![image](https://github.com/konbraphat51/module_rule/assets/101827492/136e7d9f-6b09-41f5-876b-d451dbaf3bee)

`AB` と `AC` は `AA` の**サブモジュール**なので、`AA` パッケージを以下のように作成します：

![image](https://github.com/konbraphat51/module_rule/assets/101827492/4421c74a-4ca2-4810-b1b6-fd1b8c5a4cdd)

そして、**サブパッケージの構造を隠蔽**し、`__init__.py` を使用して `AA` を **モジュールとして利用可能**にします。

`AA\__init__.py` を以下のように記述します：

```python
from A.AA.AA import aa
```

そのため、`A.py` では次のように使用できます：

```python
from A.AA import aa
```

まるで `AA` パッケージがモジュールであるかのように。

#### 5. 同位モジュールを`import`する場合は、モジュール名で`import`してください。下位モジュールを`import`する場合は、パッケージ名で`import`してください。

**モジュール名**はファイルのパスを指します。  
**パッケージ名**はディレクトリのパスを指します。

例：
![image](https://github.com/konbraphat51/module_rule/assets/101827492/4421c74a-4ca2-4810-b1b6-fd1b8c5a4cdd)

```python
# モジュール名でのインポート
from A.AA.AA import aa

# パッケージ名でのインポート
from A.AA import aa
```

もちろん、**パッケージ**名からのインポートを有効にするためには、希望する関数/クラスがパッケージの `__init__.py` に含まれている必要があります。

このルールは循環参照を避けるためのものです。

#### 6. 他最上位パッケージを`import`するときは、その最上位パッケージ名で`import`してください。同最上位パッケージを`import`するときは、モジュール名で`import`してください

この例の中で：
![image](https://github.com/konbraphat51/module_rule/assets/101827492/ec92668f-ea44-404b-b32b-d28c91220f8e)

パッケージ `A` と `Utils` は **最上位ディレクトリのモジュール** です。

`ABAA.py` が `Utils.Vector`（異なる最上位ディレクトリ）をインポートしたい場合、

```python
# パッケージ名からのインポート
from PythonRoot.Utils import Vector
```

`ABBA.py` が `ABAA.py` をインポートしたい場合（同じ最上位ディレクトリ）、

```python
# モジュール名からのインポート
from PythonRoot.A.AB.ABA.ABAA import abaa
```

このルールは循環参照を避けるためのものです。

UMLを描くのを忘れないでください。

#### UMLを描く
依存関係UMLを描く主な目的は、**循環依存を回避する**ことです。

もし次のようなサイクルを含むUMLがあれば：
![image](https://user-images.githubusercontent.com/101827492/285475790-9f48b8e2-6ea5-4f2d-9d0e-f54a92c832c6.png)

これは循環依存が存在しており、通常はシステムエラーを引き起こします。

循環依存を完全に取り除いてください。

UMLの作成には[PlantUML](https://plantuml.com/en/)の使用を推奨します。

[PlantUMLのテンプレートはこちら](https://github.com/konbraphat51/module_rule/blob/main/ModulesUML_example.puml)

## 考察
長くてごめんなさい。削ってこれです。（ドキュメンテーション力！）

モジュール構造の「上から下」の流れを押し付けつつ、他への依存関係をUMLに書くことを義務付けることによって循環参照を避けようという試みです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/ecccd379-7c46-bdb0-3f4f-d9fcf6d29c7f.png)

現在開発しているライブラリで、モジュール数が多く、左のように`import`が多かったのですが、上記ルールに従って再設計したところ、`from`がかなり低減し、見た目がスッキリしました。

まだまだ運用を始めたばかりなので、これで何か分かったことがあればここに付け足します。

## 最後に
:::note
いいね頂けると嬉しいです><

この記事を読んでいる方は次の記事も読んでいるかもしれません。
* [初心者だらけでもとりあえず共同開発ができる手引き](https://qiita.com/konbraphat51/items/8793f03807c39573fac2)
* [【Python】circular importエラーをシンプルに解説](https://qiita.com/konbraphat51/items/2b04cad627693b3c09ee)
:::