最終プロジェクト:学生成績管理(パート 2)

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


ロジック層の実装

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

例:成績統計ロジック層

PYTHON
# ====== ロジック層:成績統計と分析 ======

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
▶ 試してみよう

ロジック層のテスト

例:ロジック層の機能テスト

PYTHON
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 つのファイルに統合されます。


例外処理の設計

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

シナリオ 戻り値 説明
学生が存在しない 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 点」と扱うよりも安全です。

📖 まとめ


📝 練習問題

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

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

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

100%