はじめに
Pythonのモジュール設計、難しいですよね。
- 何を
__init__.pyでまとめるのか(まとめないと、import文がすごく多くなる) - どのようにモジュールの階層を形成するのか
-
circular importは避けなければならない
という中で、モジュールを上手にディレクトリに配置しなければなりません。
モジュール設計の指針も有名なものはなさそうなので(あればゴメンナサイ)、自分が考えた設計規則を紹介してみたいと思います。
紹介
そのまま使い回せる英語表記markdown:GitHubレポジトリへ飛ぶ
条文(和訳)
詳細は後述です。
-
基本的に下位モジュールに依存してください。メインモジュールをディレクトリ名と同じにして、メインモジュールの下位モジュールはそのディレクトリに入れてください。
-
モジュールAの下位モジュールがある場合は、それらを全て(モジュールAも下位モジュールも)パッケージAを作成して格納し、単一のモジュールとして使えるように
__init__.pyで隠蔽してください。 -
同位モジュールに依存するときは、依存関係UMLを作成/更新してください
-
他のパッケージにあるモジュールに依存する必要のある場合は、共通最下位ディレクトリの依存関係UMLを更新してください。
-
同位モジュールを
importする場合は、モジュール名でimportしてください。
下位モジュールをimportする場合は、パッケージ名でimportしてください。 -
他最上位パッケージを
importするときは、その最上位パッケージ名でimportしてください
同最上位パッケージをimportするときは、モジュール名でimportしてください
詳細(和訳)
1. 基本的に下位モジュールに依存してください。メインモジュールをディレクトリ名と同じにして、メインモジュールの下位モジュールはそのディレクトリに入れてください。
この場合、
-
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 に依存する場合、以下のようなケースでは:
lowest-common package(共通最下位パッケージ) は A_directory です。
lowest-common package の依存関係のUMLを描いてください
3. 同位モジュールに依存するときは、依存関係UMLを作成/更新してください
こちらの場合、
ABBA.py と ABBB.py は同位のモジュールです。
ABBB.py には以下のような記述があります:
from PythonRoot.A.AB.ABB.ABBA import abba
これは ABBB.py が ABBA.py に依存していることを意味します。
もしパッケージ内で、サブ→メインの依存関係以外に他の依存関係がある場合、パッケージの依存関係のUMLを描く必要があります。
この場合、ABB パッケージの依存関係のUMLは以下のようになります:
4. 他のパッケージにあるモジュールに依存する必要のある場合は、共通最下位ディレクトリの依存関係UMLを更新してください。
もしパッケージ A が以下のような構造であるとします:
AB と AC は AA のサブモジュールなので、AA パッケージを以下のように作成します:
そして、サブパッケージの構造を隠蔽し、__init__.py を使用して AA を モジュールとして利用可能にします。
AA\__init__.py を以下のように記述します:
from A.AA.AA import aa
そのため、A.py では次のように使用できます:
from A.AA import aa
まるで AA パッケージがモジュールであるかのように。
5. 同位モジュールをimportする場合は、モジュール名でimportしてください。下位モジュールをimportする場合は、パッケージ名でimportしてください。
モジュール名はファイルのパスを指します。
パッケージ名はディレクトリのパスを指します。
# モジュール名でのインポート
from A.AA.AA import aa
# パッケージ名でのインポート
from A.AA import aa
もちろん、パッケージ名からのインポートを有効にするためには、希望する関数/クラスがパッケージの __init__.py に含まれている必要があります。
このルールは循環参照を避けるためのものです。
6. 他最上位パッケージをimportするときは、その最上位パッケージ名でimportしてください。同最上位パッケージをimportするときは、モジュール名でimportしてください
パッケージ A と Utils は 最上位ディレクトリのモジュール です。
ABAA.py が Utils.Vector(異なる最上位ディレクトリ)をインポートしたい場合、
# パッケージ名からのインポート
from PythonRoot.Utils import Vector
ABBA.py が ABAA.py をインポートしたい場合(同じ最上位ディレクトリ)、
# モジュール名からのインポート
from PythonRoot.A.AB.ABA.ABAA import abaa
このルールは循環参照を避けるためのものです。
UMLを描くのを忘れないでください。
UMLを描く
依存関係UMLを描く主な目的は、循環依存を回避することです。
これは循環依存が存在しており、通常はシステムエラーを引き起こします。
循環依存を完全に取り除いてください。
UMLの作成にはPlantUMLの使用を推奨します。
考察
長くてごめんなさい。削ってこれです。(ドキュメンテーション力!)
モジュール構造の「上から下」の流れを押し付けつつ、他への依存関係をUMLに書くことを義務付けることによって循環参照を避けようという試みです。
現在開発しているライブラリで、モジュール数が多く、左のようにimportが多かったのですが、上記ルールに従って再設計したところ、fromがかなり低減し、見た目がスッキリしました。
まだまだ運用を始めたばかりなので、これで何か分かったことがあればここに付け足します。
最後に
いいね頂けると嬉しいです><
この記事を読んでいる方は次の記事も読んでいるかもしれません。


