モジュール分割って何?一言でいうと「ファイルごとに役割をはっきり分けること」
Python でいう「モジュール分割」は、
コードを複数の .py ファイルに分けて、それぞれに「はっきりした役割」を持たせることです。
1ファイルに全部書いても動きます。
でも、コード量が増えると、
どこに何が書いてあるか分からない
同じような処理があちこちにコピペされる
ちょっと直すだけで全体に影響しそうで怖い
という状態になります。
モジュール分割の目的は、
「コードを見つけやすく・理解しやすく・直しやすくすること」です。
そのために、ファイル単位で責務(役割)を分けていきます。
まずは「全部1ファイル」の状態を見てみる
before:よくある「全部 app.py に詰め込んだ」コード
例えば、簡単なタスク管理アプリを考えます。
最初はだいたい、こんな感じになりがちです。
# app.py
import json
from dataclasses import dataclass
from pathlib import Path
# モデル
@dataclass
class Task:
id: int
title: str
done: bool = False
# 永続化
DATA_FILE = Path("tasks.json")
def load_tasks() -> list[Task]:
if not DATA_FILE.exists():
return []
data = json.loads(DATA_FILE.read_text(encoding="utf-8"))
return [Task(**item) for item in data]
def save_tasks(tasks: list[Task]) -> None:
data = [task.__dict__ for task in tasks]
DATA_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
# アプリのロジック
def add_task(title: str) -> None:
tasks = load_tasks()
new_id = (max((t.id for t in tasks), default=0) + 1)
tasks.append(Task(id=new_id, title=title))
save_tasks(tasks)
def list_tasks() -> None:
tasks = load_tasks()
for task in tasks:
mark = "✔" if task.done else " "
print(f"[{mark}] {task.id}: {task.title}")
def complete_task(task_id: int) -> None:
tasks = load_tasks()
for task in tasks:
if task.id == task_id:
task.done = True
break
save_tasks(tasks)
# CLIインターフェース
def main() -> None:
import sys
if len(sys.argv) < 2:
print("usage: app.py [add|list|done] ...")
return
cmd = sys.argv[1]
if cmd == "add":
title = " ".join(sys.argv[2:])
add_task(title)
elif cmd == "list":
list_tasks()
elif cmd == "done":
complete_task(int(sys.argv[2]))
else:
print("unknown command")
if __name__ == "__main__":
main()
Pythonこれでも動きます。
でも、役割が全部混ざっています。
タスクという「モデル」
ファイルへの保存・読み込み(永続化)
アプリのロジック(追加・一覧・完了)
CLI のインターフェース
これらが1ファイルに詰め込まれているので、
コードが増えると一気にカオスになります。
モジュール分割の基本方針:「役割ごとにファイルを分ける」
どんな軸で分けると分かりやすいか
初心者にとって一番分かりやすい分け方は、
「役割(責務)」ごとに分けることです。
モデル(データ構造・ドメイン)
永続化(DB・ファイルなど)
アプリケーションのロジック(ユースケース)
インターフェース(CLI・Web・GUIなど)
さっきのタスク管理アプリを、この軸で分けてみます。
実際にモジュール分割してみる
domain.py:タスクという「ドメイン」を表すモジュール
# domain.py
from dataclasses import dataclass
@dataclass
class Task:
id: int
title: str
done: bool = False
def complete(self) -> None:
self.done = True
Pythonここには、「タスクとは何か」だけを書きます。
ファイルのことも CLI のことも知りません。
「タスクを完了にする」という振る舞いも、Task.complete としてここに閉じ込めます。
repository.py:タスクの保存・読み込みを担当するモジュール
# repository.py
import json
from pathlib import Path
from typing import Protocol
from domain import Task
class TaskRepository(Protocol):
def load(self) -> list[Task]:
...
def save(self, tasks: list[Task]) -> None:
...
class JsonTaskRepository:
def __init__(self, path: Path) -> None:
self._path = path
def load(self) -> list[Task]:
if not self._path.exists():
return []
data = json.loads(self._path.read_text(encoding="utf-8"))
return [Task(**item) for item in data]
def save(self, tasks: list[Task]) -> None:
data = [task.__dict__ for task in tasks]
self._path.write_text(
json.dumps(data, ensure_ascii=False, indent=2),
encoding="utf-8",
)
Pythonここは「タスクをどこにどう保存するか」だけを担当します。
CLI のことも、コマンド名も知りません。
TaskRepository という Protocol を定義しておくと、
あとで「SQLite版」「メモリ版」などに差し替えやすくなります。
usecase.py:アプリとして「何をするか」を表すモジュール
# usecase.py
from dataclasses import dataclass
from domain import Task
from repository import TaskRepository
@dataclass
class AddTaskInput:
title: str
def add_task(repo: TaskRepository, data: AddTaskInput) -> None:
tasks = repo.load()
new_id = (max((t.id for t in tasks), default=0) + 1)
tasks.append(Task(id=new_id, title=data.title))
repo.save(tasks)
def list_tasks(repo: TaskRepository) -> list[Task]:
return repo.load()
def complete_task(repo: TaskRepository, task_id: int) -> None:
tasks = repo.load()
for task in tasks:
if task.id == task_id:
task.complete()
break
repo.save(tasks)
Pythonここは「アプリとしての振る舞い」だけを担当します。
タスクを追加する
タスク一覧を取得する
タスクを完了にする
というユースケースを、関数として表現しています。
CLI のことも、ファイルパスも知りません。TaskRepository という抽象にだけ依存しています。
cli.py:CLIインターフェースを担当するモジュール
# cli.py
import sys
from pathlib import Path
from repository import JsonTaskRepository
from usecase import (
add_task,
list_tasks,
complete_task,
AddTaskInput,
)
def main() -> None:
repo = JsonTaskRepository(Path("tasks.json"))
if len(sys.argv) < 2:
print("usage: app.py [add|list|done] ...")
return
cmd = sys.argv[1]
if cmd == "add":
title = " ".join(sys.argv[2:])
add_task(repo, AddTaskInput(title=title))
elif cmd == "list":
tasks = list_tasks(repo)
for task in tasks:
mark = "✔" if task.done else " "
print(f"[{mark}] {task.id}: {task.title}")
elif cmd == "done":
complete_task(repo, int(sys.argv[2]))
else:
print("unknown command")
if __name__ == "__main__":
main()
Pythonここは「コマンドライン引数をどう解釈するか」「どう表示するか」だけを担当します。
タスクのルールも、保存形式も知りません。
それらは usecase と repository に任せています。
モジュール分割で何が嬉しくなるのか
「どこを見ればいいか」が一瞬で分かる
仕様変更やバグ修正が入ったときに、
どのファイルを開けばいいかが明確になります。
タスクのルールを変えたい
→ domain.py を見る
保存形式を変えたい(JSON → SQLite)
→ repository.py を見る
新しいコマンドを追加したい
→ cli.py と usecase.py を見る
1ファイルに全部詰め込んでいると、
「とりあえず app.py をスクロールしまくる」ことになります。
モジュール分割すると、その時間がごっそり減ります。
テストが書きやすくなる
例えば、ユースケースだけをテストしたいとき、
CLI やファイル I/O を気にせずに済みます。
from usecase import add_task, list_tasks, AddTaskInput
from repository import TaskRepository
from domain import Task
class InMemoryRepo(TaskRepository):
def __init__(self) -> None:
self.tasks: list[Task] = []
def load(self) -> list[Task]:
return list(self.tasks)
def save(self, tasks: list[Task]) -> None:
self.tasks = list(tasks)
def test_add_task():
repo = InMemoryRepo()
add_task(repo, AddTaskInput(title="test"))
tasks = list_tasks(repo)
assert len(tasks) == 1
assert tasks[0].title == "test"
Pythonusecase モジュールは、
「タスクのルール」と「保存の仕組み」から切り離されているので、
テスト用のリポジトリを差し替えるだけでテストできます。
これは、品質に直結します。
モジュール分割で一番大事な感覚:「一つのモジュールに一つの責務」
初心者がまず意識してほしいのは、
「1ファイルに何でもかんでも詰め込まない」ということです。
完璧な分割ルールを覚える必要はありません。
最初は、次のような感覚だけで十分です。
データ構造やビジネスルールは「domain.py」などにまとめる
外部とのやりとり(DB・ファイル・API)は「repository.py」などにまとめる
アプリとしての操作(ユースケース)は「usecase.py」にまとめる
UI(CLI・Web)は「cli.py」「web.py」などにまとめる
「このファイルは何担当?」と聞かれたときに、
一言で答えられる状態を目指すと、モジュール分割はだいたいうまくいきます。
逆に、
「このファイルは、まあ…いろいろ…」
となっていたら、
そこは分割候補です。
モジュール分割でやりがちな失敗と、その避け方
「機能ごとに分けすぎて、逆に分かりにくくなる」
例えば、こんな分け方はやりすぎです。
add_task.pylist_tasks.pycomplete_task.py
ファイルが細かすぎると、
「どこに何があるか」を覚えるのが大変になります。
一つのモジュールに「関連する機能」をまとめる方が、
結果的に見通しが良くなります。
タスク関連のユースケースは usecase.py にまとめる
タスク関連のドメインは domain.py にまとめる
くらいの粒度が、初心者にはちょうどいいです。
「名前がふわっとしていて、役割が伝わらない」
utils.pycommon.pyhelper.py
こういう名前のモジュールは、
だいたい「何でも入れていい箱」になってしまいます。
結果として、utils.py が 1000 行を超える、みたいな地獄が生まれます。
できるだけ、「何のためのモジュールか」が分かる名前を付けてください。
string_utils.pydate_utils.pytask_repository.py
など、「対象」や「責務」が分かる名前にすると、
自然と中身も整理されていきます。
初心者がモジュール分割を練習するときのおすすめステップ
まずは「1ファイルで書いてから、あとで分ける」
最初から完璧に分割しようとすると、
手が止まります。
一番いい練習は、
まずは1ファイルで素直に書く
コードが増えてきて「ごちゃっとしてきたな」と感じたら、役割ごとに分ける
という流れです。
そのときに、
モデル(ドメイン)
永続化(DB・ファイル)
ユースケース
インターフェース(CLI・Web)
のどれに近いかを考えながら、
ファイルを分けていくと、自然といい形に近づきます。
分けたあとに「import の向き」を眺めてみる
モジュール分割がうまくいっているかどうかは、import の向きを見ると分かります。
cli.py が usecase を import しているusecase.py が domain と repository を import しているrepository.py が domain を import している
このように、「外側が内側を import する」形になっていると、
設計としてかなりきれいです。
逆に、
domain.py が cli を import している
みたいな状態になっていたら、
それは依存の向きが逆です。
そのときは、「本当にそれが必要か?」を一度立ち止まって考えてみてください。
まとめ(モジュール分割は「役割ごとにファイルを分けて、未来の自分を助ける技術」)
モジュール分割を初心者目線でまとめると、こうなります。
モジュール分割は、コードを複数の .py ファイルに分けて、それぞれに「はっきりした役割(責務)」を持たせることで、コードを見つけやすく・理解しやすく・直しやすくするための技術。
モデル(ドメイン)、永続化、ユースケース、インターフェースといった軸で分けると、どこを直せばいいかが一瞬で分かるようになり、テストもしやすくなる。
完璧な分割を最初から目指す必要はなく、「まず1ファイルで書いてから、役割ごとに分ける」「utils などの曖昧な名前を避ける」「import の向きを眺めてみる」といった小さな工夫だけでも、設計と品質がかなり良くなる。

