Python: プロジェクト:成績管理(Pt.2)

レッスン 34 ではデータ層を完成させました——学生と成績の CRUD 操作です。このレッスンでは、データ層の上にロジック層を構築します:統計計算、順位付け、スコア分布分析、その他の中核ビジネスロジックです。レッスン 36 ではプレゼンテーション層(メニューインターフェース)を追加してシステムを完成させます。


1. ロジック層の実装

この層のコードは、レッスン 34 のデータ層関数(load_dataget_all_students など)に依存します。

(1) ▶ サンプル:成績統計ロジック層

TEXT 📖 参照専用
# ====== ロジック層:成績統計と分析 ======

import statistics
from grade_project_part1 import load_data, get_all_students


def calculate_student_average(student_id):
    """単一の学生の平均スコアを計算"""
    data = load_data()
    student = data["students"].get(student_id)
    if not student:
        return None

    scores = student["scores"].values()
    if not scores:
        return None

    return sum(scores) / len(scores)


def get_student_report(student_id):
    """個人成績表を生成"""
    data = load_data()
    student = data["students"].get(student_id)
    if not student:
        return None

    report = {
        "name": student["name"],
        "class_name": student["class_name"],
        "scores": dict(student["scores"]),
    }

    scores = list(student["scores"].values())
    if scores:
        report["average"] = round(sum(scores) / len(scores), 1)
        report["max_score"] = max(scores)
        report["min_score"] = min(scores)
        report["total"] = sum(scores)
    else:
        report["average"] = None
        report["max_score"] = None
        report["min_score"] = None
        report["total"] = 0

    return report


def get_class_ranking(class_name=None):
    """クラス順位を取得(合計スコア降順)"""
    data = load_data()
    students = data["students"]

    # すべての学生(または特定のクラス)を収集
    results = []
    for sid, info in students.items():
        if class_name and info["class_name"] != class_name:
            continue

        scores = list(info["scores"].values())
        total = sum(scores) if scores else 0
        avg = round(total / len(scores), 1) if scores else 0

        results.append({
            "student_id": sid,
            "name": info["name"],
            "class_name": info["class_name"],
            "total": total,
            "average": avg,
            "score_count": len(scores)
        })

    # 合計スコア降順でソート
    results.sort(key=lambda x: x["total"], reverse=True)

    # 順位を追加
    for i, r in enumerate(results, 1):
        r["rank"] = i

    return results


def get_subject_averages():
    """各科目の平均スコアを計算"""
    data = load_data()
    subjects = data["subjects"]
    students = data["students"]

    result = {}
    for subject in subjects:
        scores = []
        for info in students.values():
            if subject in info["scores"]:
                scores.append(info["scores"][subject])

        if scores:
            result[subject] = {
                "average": round(sum(scores) / len(scores), 1),
                "max": max(scores),
                "min": min(scores),
                "count": len(scores)
            }
        else:
            result[subject] = None

    return result


def get_score_distribution(subject=None):
    """スコア分布を範囲別に計算"""
    data = load_data()
    students = data["students"]

    ranges = [
        ("90-100", 90, 101),
        ("80-89", 80, 90),
        ("70-79", 70, 80),
        ("60-69", 60, 70),
        ("0-59", 0, 60),
    ]

    if subject:
        # 特定科目の分布をカウント
        result = {label: 0 for label, _, _ in ranges}
        for info in students.values():
            if subject in info["scores"]:
                score = info["scores"][subject]
                for label, low, high in ranges:
                    if low <= score < high:
                        result[label] += 1
                        break
        return result
    else:
        # 全科目を合わせた分布をカウント
        result = {label: 0 for label, _, _ in ranges}
        for info in students.values():
            for score in info["scores"].values():
                for label, low, high in ranges:
                    if low <= score < high:
                        result[label] += 1
                        break
        return result


def get_students_by_class():
    """学生をクラス別にグループ化"""
    data = load_data()
    students = data["students"]

    classes = {}
    for sid, info in students.items():
        cls = info["class_name"]
        if cls not in classes:
            classes[cls] = []
        classes[cls].append({
            "student_id": sid,
            "name": info["name"],
            "score_count": len(info["scores"])
        })

    return classes


2. ロジック層のテスト

(2) ▶ サンプル:ロジック層の機能テスト

TEXT 📖 参照専用
if __name__ == "__main__":
    print("=== Individual Report Card ===")
    report = get_student_report("2024001")
    if report:
        print(f"Name: {report['name']}")
        print(f"Class: {report['class_name']}")
        for subject, score in report["scores"].items():
            print(f"  {subject}: {score}")
        print(f"Total: {report['total']}")
        print(f"Average: {report['average']}")
        print(f"Max: {report['max_score']}")
        print(f"Min: {report['min_score']}")

    print("\n=== Class Ranking ===")
    ranking = get_class_ranking()
    for r in ranking[:5]:
        print(f"#{r['rank']}: {r['name']} ({r['class_name']}) Total {r['total']}")

    print("\n=== Subject Averages ===")
    averages = get_subject_averages()
    for subject, info in averages.items():
        if info:
            print(f"{subject}: Avg {info['average']}, Max {info['max']}, Min {info['min']}")

    print("\n=== Score Distribution ===")
    dist = get_score_distribution()
    for label, count in dist.items():
        bar = "#" * count
        print(f"{label}: {bar} {count} students")

補足: 上の import はデータ層ファイルが grade_project_part1.py という名前であることを想定しています。ファイル名が異なる場合は import 文を修正してください。最終的な完全システムでは、すべてのコードが 1 つのファイルに統合されます。



3. エラーハンドリングの設計

(3) ▶ サンプル:クラス別平均計算 (難易度 ⭐)

PYTHON
scores = {
    "2024001": {"name": "Alice", "class_name": "Class 1", "scores": {"Chinese": 85, "Math": 92, "English": 88}},
    "2024002": {"name": "Bob", "class_name": "Class 1", "scores": {"Chinese": 78, "Math": 85, "English": 90}},
    "2024003": {"name": "Charlie", "class_name": "Class 2", "scores": {"Chinese": 92, "Math": 88, "English": 95}},
    "2024004": {"name": "Diana", "class_name": "Class 2", "scores": {"Chinese": 70, "Math": 75, "English": 80}},
}

def class_averages(students):
    classes = {}
    for sid, info in students.items():
        cls = info["class_name"]
        if cls not in classes:
            classes[cls] = {}
        for subject, score in info["scores"].items():
            if subject not in classes[cls]:
                classes[cls][subject] = []
            classes[cls][subject].append(score)

    result = {}
    for cls, subjects in classes.items():
        result[cls] = {}
        for subject, vals in subjects.items():
            result[cls][subject] = round(sum(vals) / len(vals), 1)

    return result

averages = class_averages(scores)
for cls, subjects in averages.items():
    print(f"\n{cls}:")
    for subject, avg in subjects.items():
        print(f"  {subject} average: {avg}")
▶ 試してみよう

出力:

TEXT 📖 参照専用
Class 1:
  Chinese average: 81.5
  Math average: 88.5
  English average: 89.0

Class 2:
  Chinese average: 81.0
  Math average: 81.5
  English average: 87.5


3. エラーハンドリングの設計

ロジック層では以下の例外的なケースを処理します:

シナリオ 戻り値 説明
学生が存在しない None または空の結果 呼び出し側が戻り値をチェック
成績データがない 統計値が None 呼び出し側が「まだ成績がありません」と表示
空のデータ 空のリスト/空の辞書 クラッシュせず、親しみやすいメッセージを表示
無効なデータ 例外をキャッチ try-except で保護

❓ よくある質問

Q ソートの key=lambda を理解するにはどうすればよいですか?
A key パラメータはソート基準を指定します。lambda item: item[1] は「各アイテムの 2 番目の要素をソートキーとして取得する」ことを意味します。これは def get_score(item): return item[1] と同等です——lambda は単なる省略形です。
Q 0 ではなく None を返すと、呼び出し側が扱いにくくなりませんか?
A None を返すことで「スコアが存在しない」と「スコアが実際に 0」を区別できます——これは意味のある違いです。呼び出し側は if result is not None: で確認でき、「データなし」を誤って「0 点」と扱うよりも安全です。
Q データ層とロジック層を分離する具体的な利点は何ですか?
A 3 つの主要な利点があります。① テスタビリティ:ロジック層をデータ層なしで単独テストできる(モック使用)、② 再利用性:同じロジックを異なるデータソース(JSON ファイル、SQLite、API)に適用できる、③ 保守性:データ保存形式を変えてもロジックに影響しない。小規模プロジェクトでは過剰に思えますが、成長するにつれて不可欠になります。今は学べますが、実プロジェクトで必ず実感するでしょう。

📖 まとめ


📝 練習問題

  1. 基本(難易度 ⭐)get_student_report() を呼び出して 1 人の学生の完全な成績表を表示し、出力が正しいことを確認してください。

  2. 中級(難易度 ⭐⭐):ロジック層に get_top_n(n) 関数を追加してください。学校全体で合計スコアが上位 N 位までの学生を返します。

  3. 上級(難易度 ⭐⭐⭐):ロジック層に get_subject_pass_rate(subject, pass_score=60) 関数を追加してください。指定された科目の合格率(スコア >= pass_score の学生数/全学生数)を計算します。その後、全科目の合格率を計算して比較してください。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%