最終プロジェクト:学生成績管理(パート 1)
これは Python チュートリアルの最終プロジェクトです。3 回のレッスンを通じて、ゼロから完全な学生成績管理システムを構築します。プロジェクトは 3 つの層で構成されます:データ層(JSON 永続化)、ロジック層(成績処理)、プレゼンテーション層(コマンドラインインターフェース)。このレッスンでは基盤——要件とデータ層を構築します。
プロジェクト概要
TEXT
学生成績管理システム
├── レッスン 34:データ層 — 要件分析 + JSON/CSV 永続化
├── レッスン 35:ロジック層 — 成績統計 + ソート + 例外処理
└── レッスン 36:プレゼンテーション層 — メニューインターフェース + 検索 + レポート
要件仕様
TEXT
1. 学生管理
├── 学生追加(名前、学生 ID、クラス)
└── 学生削除(学生 ID で)
2. 成績管理
├── 成績入力(学生 ID で複数科目)
├── 成績修正
└── 成績レコード削除
3. 統計と分析
├── 個人成績表
├── クラス順位
├── 科目平均
└── スコア分布
4. データ永続化
├── JSON 形式でファイルに保存
└── 起動時に自動読み込み
データ構造設計
例:JSON データ構造
PYTHON
# JSON 形式で保存されるデータ
{
"students": {
"2024001": {
"name": "Zhang San",
"class_name": "Class 1",
"scores": {
"Chinese": 85,
"Math": 92,
"English": 78
}
},
"2024002": {
"name": "Li Si",
"class_name": "Class 1",
"scores": {
"Chinese": 90,
"Math": 88,
"English": 95
}
}
},
"subjects": ["Chinese", "Math", "English"],
"classes": ["Class 1", "Class 2"]
}
💡 なぜ学生 ID をキーにするのか? 学生 ID(文字列)を辞書キーにすると、ルックアップが O(1) になります——学生が 10 人でも 10,000 人でも、検索速度は同じです。
データ層の実装
例:データ層の実装とテスト
PYTHON
import json
import os
DATA_FILE = "grade_system.json"
# ====== デフォルトデータ構造 ======
DEFAULT_DATA = {
"students": {},
"subjects": ["Chinese", "Math", "English"],
"classes": []
}
# ====== データ層 ======
def load_data():
"""JSON ファイルからデータを読み込み"""
if not os.path.exists(DATA_FILE):
save_data(DEFAULT_DATA.copy())
return DEFAULT_DATA.copy()
with open(DATA_FILE, "r", encoding="utf-8") as f:
return json.load(f)
def save_data(data):
"""データを JSON ファイルに保存"""
with open(DATA_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
# ====== 学生管理 ======
def add_student(student_id, name, class_name):
"""学生を追加"""
data = load_data()
if student_id in data["students"]:
return False, f"Student ID {student_id} already exists!"
data["students"][student_id] = {
"name": name,
"class_name": class_name,
"scores": {}
}
# クラスリストを更新
if class_name not in data["classes"]:
data["classes"].append(class_name)
save_data(data)
return True, f"Added student: {name} ({student_id})"
def delete_student(student_id):
"""学生を削除"""
data = load_data()
if student_id not in data["students"]:
return False, f"Student ID {student_id} does not exist!"
name = data["students"][student_id]["name"]
del data["students"][student_id]
save_data(data)
return True, f"Deleted student: {name}"
def get_student(student_id):
"""単一の学生情報を取得"""
data = load_data()
return data["students"].get(student_id)
def get_all_students():
"""すべての学生を取得"""
data = load_data()
return data["students"]
# ====== 成績管理 ======
def add_score(student_id, subject, score):
"""成績を入力"""
data = load_data()
if student_id not in data["students"]:
return False, f"Student ID {student_id} does not exist!"
if subject not in data["subjects"]:
return False, f"Subject {subject} does not exist!"
if not isinstance(score, (int, float)) or score < 0 or score > 100:
return False, "Score must be between 0 and 100!"
data["students"][student_id]["scores"][subject] = score
save_data(data)
return True, f"Entered {data['students'][student_id]['name']}'s {subject} score: {score}"
def update_score(student_id, subject, score):
"""成績を更新(add_score と同じ機能)"""
return add_score(student_id, subject, score)
def delete_score(student_id, subject):
"""科目の成績を削除"""
data = load_data()
if student_id not in data["students"]:
return False, f"Student ID {student_id} does not exist!"
if subject not in data["students"][student_id]["scores"]:
return False, f"{data['students'][student_id]['name']} has no {subject} score"
del data["students"][student_id]["scores"][subject]
save_data(data)
return True, f"Deleted {data['students'][student_id]['name']}'s {subject} score"
# ====== テストコード ======
if __name__ == "__main__":
print("=== Testing Data Layer ===")
print(add_student("2024001", "Zhang San", "Class 1"))
print(add_student("2024002", "Li Si", "Class 1"))
print(add_student("2024003", "Wang Wu", "Class 2"))
print(add_score("2024001", "Chinese", 85))
print(add_score("2024001", "Math", 92))
print(add_score("2024002", "Chinese", 90))
student = get_student("2024001")
print(f"\nZhang San's info: {student}")
print(f"\nTotal students: {len(get_all_students())}")
設計のポイント
- 統一された関数の戻り値:各関数は
(bool, str)タプルを返します——Trueは成功、Falseは失敗、文字列はメッセージです - 繰り返し操作のカプセル化:
load_data()とsave_data()関数はすべてのデータ操作で再利用 - 入力検証:スコア範囲チェックと学生 ID 存在確認はデータ層で行われるため、上位層で繰り返す必要がない
- 拡張準備完了:レッスン 35 と 36 でこのデータ層の上に機能を追加
❓ よくある質問
Q なぜ名前ではなく学生 ID を辞書キーにするのですか?
A 名前は重複する可能性がありますが(同名の学生)、学生 ID は一意の識別子です。辞書キーは一意である必要があり、学生 ID を使用すると O(1) の検索速度が保証され、競合も発生しません。
Q JSON ファイルパスをハードコードするのは良い習慣ですか?
A チュートリアルでは簡略化のためハードコードしています。実際のプロジェクトでは、設定ファイルやコマンドライン引数でパスを指定してください。
os.path.join() でパスを構築し、ハードコードを避けましょう。Q 例外を発生させる代わりに
(success, message) タプルを返すのはなぜですか?A これは「ステータスコードパターン」です——呼び出し側は
success に基づいて何を行うかを決定でき、try/except よりも軽量です。小規模な CLI ツールに適しています。大規模プロジェクトではカスタム例外クラスが推奨されます。📖 まとめ
- プロジェクトは 3 層アーキテクチャを使用:データ層 → ロジック層 → プレゼンテーション層。このレッスンではデータ層を完成
- データは JSON 形式で永続化。学生 ID が辞書キーとして O(1) 検索を実現
- スコアは 0~100 の範囲で、入力時に検証
- すべての操作関数は
(success, message)タプルを返し、呼び出し側で処理しやすい
📝 練習問題
-
基本(難易度 ⭐):上記のデータ層コードを実行し、2~3 人の学生とその成績を追加し、JSON ファイルが正しく生成されることを確認してください。
-
中級(難易度 ⭐⭐):データ層に
update_student(student_id, name=None, class_name=None)関数を追加してください。指定されたフィールドのみを更新し(その他は変更しない)、動作することを確認します。 -
上級(難易度 ⭐⭐⭐):
add_scoreに一括入力機能を追加してください——{"Chinese": 85, "Math": 92}のような辞書を受け取り、複数の成績を一度に入力します。要件:いずれかのスコアが無効な場合、バッチ操作全体をロールバックします(データを保存しない)。



