3
1

はじめに

Pythonのモジュール設計、難しいですよね。

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

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

紹介

そのまま使い回せる英語表記markdownGitHubレポジトリへ飛ぶ

条文(和訳)

詳細は後述です。

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

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

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

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

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

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

詳細(和訳)

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

image

この場合、

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

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

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

lowest-common(最も低い共通) とは次の意味です:

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

A_A_AA_B_B に依存する場合、以下のようなケースでは:

Image

lowest-common package(共通最下位パッケージ)A_directory です。

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

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

image

こちらの場合、

ABBA.pyABBB.py同位のモジュールです。

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

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

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

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

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

image

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

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

image

ABACAAサブモジュールなので、AA パッケージを以下のように作成します:

image

そして、サブパッケージの構造を隠蔽し、__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してください。

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

例:
image

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

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

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

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

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

この例の中で:
image

パッケージ AUtils最上位ディレクトリのモジュール です。

ABAA.pyUtils.Vector(異なる最上位ディレクトリ)をインポートしたい場合、

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

ABBA.pyABAA.py をインポートしたい場合(同じ最上位ディレクトリ)、

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

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

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

UMLを描く

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

もし次のようなサイクルを含むUMLがあれば:
image

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

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

UMLの作成にはPlantUMLの使用を推奨します。

PlantUMLのテンプレートはこちら

考察

長くてごめんなさい。削ってこれです。(ドキュメンテーション力!)

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

image.png

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

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

最後に

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

この記事を読んでいる方は次の記事も読んでいるかもしれません。

3
1
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
3
1