Python | Web / API:モジュールの分割

Python Python
スポンサーリンク

概要(モジュールの分割は「責務で切り分けて再利用性と保守性を上げる」技術)

モジュール分割は、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 で入口を整えることです。段階的に小さく切り、分割直後に動作確認とテストを挟む。依存の向きを一方向に揃え、循環は設計で潰し、やむを得なければ遅延インポートで回避する。公開面は短く安定させ、内部は自由に入れ替える。この型を体に入れれば、初心者でも短いステップで「読みやすい・壊れにくい・進化しやすい」モジュール分割が実現できます。

タイトルとURLをコピーしました