プログラミングを学び、徐々に複雑で長いコードが書けるようになる。
その辺りで必ず登場するのが、モジュール分割の話です。
コード継ぎ足しで書いていくと、長くてさすがに読みにくい。
なにかしら、コードを分けて管理しないと。
でもどうやって?どんな考えで分割すればいいの?
簡単な答えはありません。状況によっても変わります。
でも、押さえておくべき基本的な考えはあるのです。
モジュール分割ってなに?
世間でよく見る、モジュール分割の説明は、こんな感じ。
なんか、ふわっとした説明。もう少し、具体的にイメージしましょう。
そもそも「モジュール」って?
そもそも思う疑問がこれ。
モジュールってなんなのさ?
独立した部品って言われても、機械とかじゃないんだから。
なにかしら、まとまった単位のことではあるんでしょう。プログラミングでいうと、こんなもの?
- 関数やメソッド
- クラス
- ソースファイル
- ライブラリ
実は、言ってしまえば、それぞれが全部モジュール、だとは言えます。
モジュール=ソースファイル、でイメージしよう
全部がモジュール、だと身も蓋もありません。具体的に定義しましょう。
よく見かけるのはこのイメージ。
「モジュール分割」=「ソースファイル分割」
関数やメソッドなら関数分割。クラスならクラス分割。複数ソースファイルになるとパッケージ分割。という言い方のほうがしっくりきますね。
以降では、モジュール分割=ソースファイル分割と考えて、説明していきます。
サンプルコードで考えよう
つらつら文章を並べてもイメージわきません。サンプルを見て考えましょう。
例題:成績管理プログラム
簡単な成績管理プログラムで考えましょう。
- 生徒の成績を管理するプログラム
- 操作はコマンドで行う。生徒の登録や、平均点の算出ができる
- 生徒データは終了時に保存できる。保存データは、次の起動時に自動的に読み込む
Pythonで作ってみました。
import json
def main():
# 生徒データ
students = []
# 生徒データファイル
filename = "database.json"
# 生徒データファイルを読み込む
try:
with open(filename, 'r', encoding='utf-8') as file:
students = json.load(file)
except FileNotFoundError:
print(f"ファイル {filename} がありません。空リストで起動します。")
except Exception as e:
print(f"ファイル読み込みに失敗しました。 {e}")
# メニュー処理
while True:
print("\nメニュー:")
print("1. 生徒を追加")
print("2. 生徒データを表示")
print("3. 平均点を表示")
print("4. 終了")
choice = input("選択してください: ")
# 生徒を追加
if choice == '1':
name = input("生徒名: ")
scores = input("生徒の点数 (コンマ区切り): ")
scores = [float(score) for score in scores.split(',')]
students.append({"name": name, "scores": scores})
# 生徒データを表示
elif choice == '2':
for student in students:
name = student["name"]
scores = student["scores"]
average = sum(scores) / len(scores)
print(f"生徒: {name}, 点数: {scores}, 平均点: {average:.2f}")
# 平均点を表示
elif choice == '3':
total_scores = []
for student in students:
total_scores.extend(student["scores"])
if total_scores:
average_score = sum(total_scores) / len(total_scores)
print(f"全生徒の平均点: {average_score:.2f}")
else:
print("データがありません。")
# 終了
elif choice == '4':
break
else:
print("選択が違います。やり直してください。")
# 生徒データファイルを保存する
try:
with open(filename, 'w', encoding='utf-8') as file:
json.dump(students, file, ensure_ascii=False, indent=4)
print(f"生徒データを {filename} に保存しました。")
except Exception as e:
print(f"ファイル書き込みに失敗しました。 {e}")
if __name__ == "__main__":
main()
動作イメージ
ファイル database.json がありません。空リストで起動します。
メニュー:
1. 生徒を追加
2. 生徒データを表示
3. 平均点を表示
4. 終了
選択してください: 1
生徒名: 山田
生徒の点数 (コンマ区切り): 52,38,64
メニュー:
1. 生徒を追加
2. 生徒データを表示
3. 平均点を表示
4. 終了
選択してください: 1
生徒名: 田中
生徒の点数 (コンマ区切り): 79,94,65
メニュー:
1. 生徒を追加
2. 生徒データを表示
3. 平均点を表示
4. 終了
選択してください: 2
生徒: 山田, 点数: [52.0, 38.0, 64.0], 平均点: 51.33
生徒: 田中, 点数: [79.0, 94.0, 65.0], 平均点: 79.33
メニュー:
1. 生徒を追加
2. 生徒データを表示
3. 平均点を表示
4. 終了
選択してください: 3
全生徒の平均点: 65.33
メニュー:
1. 生徒を追加
2. 生徒データを表示
3. 平均点を表示
4. 終了
選択してください: 4
生徒データを database.json に保存しました。
いまは、
- main関数1つ
- ソースファイル1つ
の状態です。
モジュール分割してみよう
これだけのコードの長さ、そろそろ読みやすさも限界な感じです。
このプログラムを、モジュール分割(ソースファイル分割)してみましょう。
といっても、main関数ひとつだと、ファイルに分けようがないですね。
まずは関数分割し、次にソースファイル分割していきます。
モジュール分割の大事な考え:「くりぬく」
モジュール分割にあたり、最も重要な基本を確認。
これ、大事な概念です。
「切る」のではなく「くりぬく」
プログラムを分割しよう…としたときに、ここの部分で切り分けよう、とか考えがち。
ここの位置で、スパッと切り分けよう

これ、違うんです。
モジュール分割といったときのイメージは、これです。
ここからここの範囲を、くりぬこう

まず、「くりぬく」というイメージが大事です。
分割した後も動きを変えないこと
なぜ、切り分けるのだと、ダメなのでしょう。
モジュール分割するときに、守らなければいけないことが、1つあります。
プログラムはたいてい、こういう構成になっています。
- なにかしらを入力して
- なんやかんややって
- なにかしらを出力する
ここで、「同じ動き」というのは、すなわち「同じ入力をしたら、同じ出力を返す」こと。
そうすると、コードの最初と最後は、変えられないわけです。途中で切ったら、入力や出力が変わっちゃいますからね。
だからモジュール分割とは、前後に切るのでなく、中間をくりぬく、というイメージになるのです。
まずは、関数を分割してみよう
例題のコードで、まずは関数分割をしてみます。
ある部分をくりぬいて、別の関数にするわけですが…どこをくりぬけばいいの?
どこでもいいってわけじゃありません、基本的な考え方があります。
考え方1:入力、変換、出力に分けてくりぬく
モジュール分割しても「同じ入力をしたら、同じ出力を返す」のが大事。
で、プログラムはたいてい、こういう構造。
- なにかしらを入力して
- なんやかんややって
- なにかしらを出力する
ここからイメージできる、くりぬく場所は、「入力」「変換」「出力」この3つ、です。
例題で考えると、こんな感じ。
- 生徒データファイルを読み込んで
- いろいろなメニューを処理して
- 生徒データファイルを保存する
さっそく分割してみましょう。mainの処理を分割してみます。
- load_students(生徒データ読み込み)
- processing_menu(メニュー処理)
- save_students(生徒データ保存)
このようなコードになります。
import json
def load_students(filename):
"""
生徒データ読み込み
"""
try:
with open(filename, 'r', encoding='utf-8') as file:
students = json.load(file)
return students
except FileNotFoundError:
print(f"ファイル {filename} がありません。空リストで起動します。")
return []
except Exception as e:
print(f"ファイル読み込みに失敗しました。 {e}")
return []
def save_students(filename, students):
"""
生徒データ保存
"""
try:
with open(filename, 'w', encoding='utf-8') as file:
json.dump(students, file, ensure_ascii=False, indent=4)
print(f"生徒データを {filename} に保存しました。")
except Exception as e:
print(f"ファイル書き込みに失敗しました。 {e}")
def processing_menu(students):
"""
メニュー処理
"""
while True:
print("\nメニュー:")
print("1. 生徒を追加")
print("2. 生徒データを表示")
print("3. 平均点を表示")
print("4. 終了")
choice = input("選択してください: ")
# 生徒を追加
if choice == '1':
name = input("生徒名: ")
scores = input("生徒の点数 (コンマ区切り): ")
scores = [float(score) for score in scores.split(',')]
students.append({"name": name, "scores": scores})
# 生徒データを表示
elif choice == '2':
for student in students:
name = student["name"]
scores = student["scores"]
average = sum(scores) / len(scores)
print(f"生徒: {name}, 点数: {scores}, 平均点: {average:.2f}")
# 平均点を表示
elif choice == '3':
total_scores = []
for student in students:
total_scores.extend(student["scores"])
if total_scores:
average_score = sum(total_scores) / len(total_scores)
print(f"全生徒の平均点: {average_score:.2f}")
else:
print("データがありません。")
# 終了
elif choice == '4':
break
else:
print("選択が違います。やり直してください。")
def main():
# 生徒データ
students = []
# 生徒データファイル
filename = "database.json"
# 生徒データファイルを読み込む
students = load_students(filename)
# メニュー処理
processing_menu(students)
# 生徒データファイルを保存する
save_students(filename, students)
if __name__ == "__main__":
main()
すこし見やすくなりましたね。
ひとつの処理を
- 入力(Source)
- 変換(Transfer)
- 出力(Sink)
の3つに分ける手法を、STS分割といいます
考え方2:操作単位でくりぬく
もう少し関数を分割してみましょう。
やりかたでもうひとつ思いつくのは、メニュー処理の部分。
- 生徒データを表示
- 平均点を表示
それぞれのメニュー処理は、完結した一連の処理。なので、くりぬきやすいでしょう。
メニュー処理をそれぞれ、分割してみます。
- add_students(生徒追加)
- display_students(生徒データ表示)
- display_average_score(平均点表示)
結果はこうなります。
import json
def load_students(filename):
"""
生徒データ読み込み
"""
try:
with open(filename, 'r', encoding='utf-8') as file:
students = json.load(file)
return students
except FileNotFoundError:
print(f"ファイル {filename} がありません。空リストで起動します。")
return []
except Exception as e:
print(f"ファイル読み込みに失敗しました。 {e}")
return []
def save_students(filename, students):
"""
生徒データ保存
"""
try:
with open(filename, 'w', encoding='utf-8') as file:
json.dump(students, file, ensure_ascii=False, indent=4)
print(f"生徒データを {filename} に保存しました。")
except Exception as e:
print(f"ファイル書き込みに失敗しました。 {e}")
def add_student(name, scores, students):
"""
生徒を追加
"""
scores = [float(score) for score in scores.split(',')]
students.append({"name": name, "scores": scores})
def display_students(students):
"""
生徒データ表示
"""
for student in students:
name = student["name"]
scores = student["scores"]
average = sum(scores) / len(scores)
print(f"生徒: {name}, 点数: {scores}, 平均点: {average:.2f}")
def display_average_scores(students):
"""
平均点表示
"""
total_scores = []
for student in students:
total_scores.extend(student["scores"])
if total_scores:
average_score = sum(total_scores) / len(total_scores)
print(f"全生徒の平均点: {average_score:.2f}")
else:
print("データがありません。")
def processing_menu(students):
"""
メニュー処理
"""
while True:
print("\nメニュー:")
print("1. 生徒を追加")
print("2. 生徒データを表示")
print("3. 平均点を表示")
print("4. 終了")
choice = input("選択してください: ")
# 生徒を追加
if choice == '1':
name = input("生徒名: ")
scores = input("生徒の点数 (コンマ区切り): ")
add_student(name, scores)
# 生徒データを表示
elif choice == '2':
display_students(students)
# 平均点を表示
elif choice == '3':
display_average_scores(students)
# 終了
elif choice == '4':
break
else:
print("選択が違います。やり直してください。")
def main():
# 生徒データ
students = []
# 生徒データファイル
filename = "database.json"
# 生徒データファイルを読み込む
students = load_students(filename)
# メニュー処理
processing_menu(students)
# 生徒データファイルを保存する
save_students(filename, students)
if __name__ == "__main__":
main()
ひとつひとつの関数が、すっきりしてきました。
完結した処理単位で分割する手法を、トランザクション分割(TR分割)といいます。
次はソースファイルを分割してみよう
関数単位の分割はできました。
次は、ソースファイルの分割です。関数を、複数のソースに振り分けます。
まあ正直、どこのソースにどの関数を入れても、プログラムとしては動きます。
でもやっぱり、適当にやっていいわけじゃない。これも基本的な考え方があります。
考え方:「意図」と「実装」に分ける
ソースファイル分割でよく使われる考えが、これです。
こういう解釈になります。
- 意図:(本当に)やりたいこと
- 実装:(どうしても)やらないといけないこと
…やらないといけないこと?いやいや全部、やらないといけないことなのでは?
意図なのか実装なのかは、仕様の要約で確認する
「本当にやりたいこと」と「どうしてもやらないといけないこと」の違いって、なんでしょう?
それを見分けるいい方法は、「そのプログラムの仕様を見る」ことです。
仕様をあらためて確認しましょう。
- 生徒の成績を管理するプログラム
- 操作はコマンドで行う。生徒の登録や、平均点の算出ができる
- 生徒データは終了時に保存できる。保存データは、次の起動時に自動的に読み込む
ここで、
- 仕様に必ず登場するのが、「やりたいこと」
- 仕様に必ずしも登場しないのが、「やらないといけないこと」
ということになります。
例えばこれ。
- 生徒データを保存する
- 平均点を算出する
これは、実際にそういうことをしたいので、「やりたいこと」。
一方、
- JSON形式で
- database.jsonへ保存する
これは仕様に直接登場しません。でも、やる必要はあるので、「やらないといけないこと」。
仕様に直接登場しない「やらなければいけないこと」を、別のモジュール(=ソースファイル)に切り分けます。
例題でいうと、このあたりの関数ですね。
- load_students(生徒データ読み込み)
- save_students(生徒データ保存)
なぜ意図と実装に分けるのか?
意図と実装に分ける理由は、なんでしょう?その違いは「変更のされやすさ」です。
- 「意図」は、変更されにくい 生徒データを管理するという、本来やりたいことは変わらない
- 「実装」は、変更されやすい データがいっぱいになったら、保存先を変えたりとか
ソフトウェアには、常に変更がつきもの。そのときに、変更の影響をできるだけ抑えたい。
変更されにくい箇所/されやすい箇所を、モジュールに切り分ける。これで、できるだけ手が入る範囲を限定させるのです。
実際に分割してみよう
それでは例題を、実際にモジュール分割してみましょう。
ソースファイルを、次のように分けます。
- 「意図」を表現する、main.py
- 「実装」を表現する、student_data.py
student_data.pyに、load_studentsとsave_studentsを移動します。
import json
def load_students(filename):
"""
生徒データ読み込み
"""
try:
with open(filename, 'r', encoding='utf-8') as file:
students = json.load(file)
return students
except FileNotFoundError:
print(f"ファイル {filename} がありません。空リストで起動します。")
return []
except Exception as e:
print(f"ファイル読み込みに失敗しました。 {e}")
return []
def save_students(filename, students):
"""
生徒データ保存
"""
try:
with open(filename, 'w', encoding='utf-8') as file:
json.dump(students, file, ensure_ascii=False, indent=4)
print(f"生徒データを {filename} に保存しました。")
except Exception as e:
print(f"ファイル書き込みに失敗しました。 {e}")
main.pyには、save_studentsの関数を使うためのfromを追加します。
from student_data import load_students, save_students
def add_student(name, scores, students):
"""
生徒を追加
"""
scores = [float(score) for score in scores.split(',')]
students.append({"name": name, "scores": scores})
def display_students(students):
"""
生徒データ表示
"""
for student in students:
name = student["name"]
scores = student["scores"]
average = sum(scores) / len(scores)
print(f"生徒: {name}, 点数: {scores}, 平均点: {average:.2f}")
def display_average_scores(students):
"""
平均点表示
"""
total_scores = []
for student in students:
total_scores.extend(student["scores"])
if total_scores:
average_score = sum(total_scores) / len(total_scores)
print(f"全生徒の平均点: {average_score:.2f}")
else:
print("データがありません。")
def main():
# 生徒データ
students = []
# 生徒データファイル
filename = "database.json"
# 生徒データファイルを読み込む
students = load_students(filename)
# メニュー処理
while True:
print("\nメニュー:")
print("1. 生徒を追加")
print("2. 生徒データを表示")
print("3. 平均点を表示")
print("4. 終了")
choice = input("選択してください: ")
# 生徒を追加
if choice == '1':
name = input("生徒名: ")
scores = input("生徒の点数 (コンマ区切り): ")
add_student(name, scores)
# 生徒データを表示
elif choice == '2':
display_students(students)
# 平均点を表示
elif choice == '3':
display_average_scores(students)
# 終了
elif choice == '4':
break
else:
print("選択が違います。やり直してください。")
# 生徒データファイルを保存する
save_students(filename, students)
if __name__ == "__main__":
main()
モジュール分割が、無事できました!
モジュール分割のときに気をつけること
このような手順でモジュール分割していくわけですが、大事なポイントをもう少し説明します。
モジュールの依存関係を単方向にすること
モジュール分割すると、そのモジュールの間には、依存関係が生まれます。
依存関係とは、このような定義。
- モジュールAにある関数が
- モジュールBにある関数を呼び出しているとき
→「AはBに依存している」
Bがないと、Aは動かないわけですからね。
このとき、
- AがBに依存している
- BがAに依存している
のような、お互いに依存している関係にしてはいけません。
依存元の変更があったとき、依存先にまで影響が及ぶ可能性があります。モジュールはできるだけ、他に依存しないほうがよいのです。
両方向に依存してしまうと、お互いの影響が他方に及んでしまいます。
そのモジュールが「知るべきでないこと」を決めること
例えば、メインモジュールmain.py。
生徒データの保存はやっています。が、それが具体的にどのように行われているかは知りません。「どんなファイル名か」「どのような形式で保存されているか」は、知る必要のない知識です。
- 生徒データが保存されることは知っている
- 保存形式の詳細は知らない
他方で、生徒データモジュールstudent_data.py。
具体的な保存の処理をやっています。が、それがいつどのように使われるかは知りません。「どのような操作で変更されたのか」は、知る必要のない知識です。
- 生徒データの保存や読み込み方法は知っている
- どのような操作で変更されるのかは知らない
モジュール分割するうえで、「知るべき知識」はイメージしやすいのです。やることそのものですからね。
一方、「知るべきでない知識」にはなかなか目が届かない。でもこれ、ちゃんと線引きしないと、どんどん境界が曖昧になっていきます。
多くのことを知っているモジュールは、それだけ他の影響を受けやすくなります。影響を受ける境界を意識しておきましょう。
モジュールが「知るべきでないこと」を決めておけば、新しい関数を作ったときでも、どこのモジュールに入れるべきか判断しやすくなりますね。
モジュール分割することの意義
モジュール分割によって、見通しのよいコードになりました。
モジュール分割の最大のメリットは「分かりやすくなる」です。が、さらにほかの意義もあるのです。
何かあったときの影響範囲が絞られる
プログラミングには、常に修正がつきまといます。
バグの発生、仕様の変更。修正したからには、その影響範囲を確認しないといけません。
このとき、お互い影響を受けにくいモジュール構成になっていれば、修正が及ぶ範囲を限定的にできるのです。
修正していないモジュールには、基本的には影響はないはずですからね。
複数人で同時開発できる
ソースファイルを分割すれば、各々のソースファイルを、違う人が編集できます。
そう、モジュール分割は、複数人でプログラム開発するときには必須なのです。
モジュール分割には、大きな問題を複数の小さな問題に分割する、という意味があります。
それぞれの小さな問題をそれぞれ解決すれば、大きな問題が解決できる。
このような考えを「分割統治」と言います。
まとめ
プログラミングで避けられない、モジュール分割の話。
決まった正解はなく、ケースバイケースであることは否めません。
といっても、なんとなく分割した、では後で大変なことになります。
大事なことは、「どういう考え方で分割したのか」を、ちゃんと説明できること。
基本的な考え方を踏まえつつ、丁寧にモジュール分割をしていきましょう。


コメント