概要(モジュールの分割は「責務で切り分けて再利用性と保守性を上げる」技術)
モジュール分割は、1つの長いスクリプトを機能ごとにファイルへ分け、明確な境界でインポートし合う設計です。初心者がまず押さえるのは、何を単位に分けるか(責務)、どう参照するか(絶対インポート中心)、分割後の動作を壊さないための入口設計(init.py と name)です。ポイントは「入力・処理・出力を分ける」「API面を整える」「循環インポートを避ける」。これを型として覚えると、見通しの良いコードに一気に進化します。
どこで分けるか(責務の線引きと切り出しの順序)
責務の分離(入出力・ドメイン・外部連携)
最初の分割は「入力」「処理」「出力」を別ファイルに切るのが確実です。例えばCLIやHTTPを受ける入力、計算やバリデーションなどのドメイン処理、ログやDB・APIなどの外部連携は、変更頻度も依存も異なるため、境界を引きやすい領域です。次に「共通ユーティリティ(文字列整形、日付処理)」と「設定読み込み」を別モジュールへ。これだけで修正箇所の特定とテストの単位が明確になります。
小さく切って動作維持(段階的リファクタ)
一度に全部を分けると壊れやすいので、関数やクラスを小さく切って別ファイルへ移し、元のコードからインポートして置き換えます。毎回、移した直後に動作確認を挟み、テストを追加して次の分割へ進みます。この「小さく切る→動くことを確認」が失敗しないモジュール分割の鉄則です。
インポートの型(絶対インポートを基本に、相対は最小限)
絶対インポートが基本
パッケージ名から辿る絶対インポートは、構造の変更に強く、検索もしやすいので原則としてこちらを使います。from myapp.services.user import get_user のように、パッケージの入口から機能までを明示します。プロジェクト内の参照関係が一目でわかり、循環インポートの早期発見にも役立ちます。
相対インポートは近所参照だけに限定
相対は、同じパッケージ内の近いモジュールを指すときにだけ使うと混乱しません。from .utils import slugify のように最小限に留め、深い階層をまたぐ相対参照は避けます。相対の連鎖は構造変更で壊れやすく、保守を難しくします。
入口の整え方(init.py と name の要点)
init.py で「使われる面」を短くする
パッケージの init.py に、よく使う関数やクラスを再公開すると、利用側のインポートが短くなります。内部構造をあとで入れ替えても、公開面は安定します。from .math_utils import add のように再輸出して、from myapp import add と使わせる設計は、利用者体験をスッキリさせます。
name で直接実行とimportを分ける
if name == “main“: にエントリ処理(簡易デモやCLI起動)を置けば、モジュールをインポートしたときに副作用が走りません。分割後の「思わぬ実行」を防ぎ、テストや再利用での事故を減らします。直接実行は python -m myapp.cli の形に統一すると、パス問題も避けられます。
例題(モノリスを段階的に3分割する)
例題1:入出力・処理・共通を分ける最小構成
myapp/
__init__.py
io_layer.py # 入出力(CLIやHTTP受け)
domain.py # ドメイン処理(検証・計算)
common.py # 共通ユーティリティ
main.py # エントリ
# domain.py
def validate_age(age: int) -> bool:
return age >= 0
def calc_dog_year(age: int) -> int:
return age * 7
Python# common.py
def slugify(s: str) -> str:
return s.lower().replace(" ", "-")
Python# io_layer.py
from myapp.domain import validate_age, calc_dog_year
from myapp.common import slugify
def greet(name: str, age: int) -> str:
if not validate_age(age):
return "年齢が不正です"
return f"{slugify(name)} さんのドッグイヤーは {calc_dog_year(age)} 歳"
Python# main.py
from myapp.io_layer import greet
if __name__ == "__main__":
print(greet("Hello World", 5))
Pythonこの分割で、出入り口(io_layer)とロジック(domain)、共通処理(common)が明確になり、修正の影響範囲が限定されます。
例題2:サブパッケージで外部連携を分離
myapp/
__init__.py
domain.py
common.py
services/
__init__.py
db.py # DB接続
api.py # 外部API
main.py
# services/db.py
def connect(url: str) -> str:
return f"DB connected: {url}"
Python# services/api.py
def get_user(uid: str) -> dict:
return {"id": uid, "name": "taro"}
Python# domain.py
from myapp.services.api import get_user
from myapp.services.db import connect
def user_profile(uid: str) -> dict:
conn = connect("sqlite:///local.db")
user = get_user(uid)
return {"user": user, "conn": conn}
Python外部連携を services サブパッケージに集約すると、通信や接続の変更がドメイン層へ波及しにくくなります。テストでは services をモックに差し替えるだけで、ドメイン層を純粋に検証できます。
循環インポート対策(依存の向きと遅延解決)
依存の向きを「上から下」へ統一する
循環は「A が B を、B が A を」参照すると発生します。層を決めて、上位(入出力)→ドメイン→下位(外部連携・共通)という一方向だけにします。共通はどこからでも参照されますが、共通がドメインへ依存するのは避けます。依存の向きが一貫していれば、循環は自然に消えます。
遅延インポートや関数引数で解決する
どうしても相互参照が必要なら、関数内でインポートして「実行時に読み込む」遅延解決に切り替えます。または、関数引数で必要な機能を受け渡し、モジュールの直接依存を断ちます。これでモジュールレベルの読み込み時循環を回避できます。
テストと運用(分割の効果を最大化する)
単体テストの配置と独立性
tests/ 配下にモジュールごとのテストを置き、外部連携はモックで差し替えます。分割されたドメイン層が純粋であれば、テストは高速で壊れにくく、変更に強くなります。テストの早期失敗が分割の品質を引き上げます。
パブリックAPIの固定と内部の自由度
利用者が触る入口(init.py から再公開する関数・クラス)は安定させ、内部構造は必要に応じて入れ替えます。公開面の互換性を保てば、分割の追加・調整を安全に続けられます。これは「壊さず進化する」ための実務的なコツです。
まとめ(責務分離・絶対インポート・入口設計を型にする)
モジュール分割の核心は、責務で切り分け、絶対インポートで参照を明快にし、init.py と name で入口を整えることです。段階的に小さく切り、分割直後に動作確認とテストを挟む。依存の向きを一方向に揃え、循環は設計で潰し、やむを得なければ遅延インポートで回避する。公開面は短く安定させ、内部は自由に入れ替える。この型を体に入れれば、初心者でも短いステップで「読みやすい・壊れにくい・進化しやすい」モジュール分割が実現できます。
