Final Project: Student Grade Management (Part 2)

In Lesson 34, we completed the data layer — CRUD operations for students and grades. In this lesson, we build the logic layer on top of the data layer: statistical calculations, ranking, score distribution analysis, and other core business logic. Lesson 36 will add the presentation layer (menu interface) to complete the system.


Logic Layer Implementation

This layer's code depends on the data layer functions from Lesson 34 (load_data, get_all_students, etc.).

Example: Grade Statistics Logic Layer

PYTHON
# ====== Logic Layer: Grade Statistics and Analysis ======

import statistics
from grade_project_part1 import load_data, get_all_students


def calculate_student_average(student_id):
    """Calculate a single student's average score"""
    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):
    """Generate an individual report card"""
    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):
    """Get class ranking (by total score descending)"""
    data = load_data()
    students = data["students"]

    # Collect all students (or a specific class)
    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)
        })

    # Sort by total descending
    results.sort(key=lambda x: x["total"], reverse=True)

    # Add ranking
    for i, r in enumerate(results, 1):
        r["rank"] = i

    return results


def get_subject_averages():
    """Calculate average scores for each subject"""
    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):
    """Calculate score distribution across ranges"""
    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:
        # Count distribution for a specific 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:
        # Count distribution across all subjects combined
        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():
    """Group 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
▶ Try it Yourself

Logic Layer Testing

Example: Logic Layer Functional Test

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")
▶ Try it Yourself

Note: The import above assumes the data layer file is named grade_project_part1.py. Modify the import statement if your filename differs. In the final complete system, all code will be merged into one file.


Exception Handling Design

The logic layer handles the following exceptional cases:

Scenario Return Value Description
Student doesn't exist None or empty result Caller checks the return value
No grade data Statistics value is None Caller displays "No grades yet"
Empty data Empty list/empty dict Won't crash, displays friendly message
Invalid data Exception caught Protected by try-except

FAQ

Q How do I understand key=lambda in sorting?
A The key parameter specifies the sorting criterion. lambda item: item[1] means "take the second element of each item as the sorting key." This is equivalent to def get_score(item): return item[1] — lambda is just shorthand.
Q Doesn't returning None instead of 0 make things harder for the caller?
A Returning None distinguishes between "no score exists" and "the score is actually 0" — a meaningful difference. The caller can use if result is not None: to check, which is safer than mistakenly treating "no data" as "0 points."

Summary


Exercises

  1. Beginner (Difficulty: Star): Call get_student_report() to view a complete report card for one student and verify the output is correct.

  2. Intermediate (Difficulty: Star-Star): Add a get_top_n(n) function to the logic layer that returns the top N students by total score across the entire school.

  3. Advanced (Difficulty: Star-Star-Star): Add a get_subject_pass_rate(subject, pass_score=60) function to the logic layer that calculates the pass rate for a given subject (students with score >= pass_score / total students). Then calculate and compare pass rates across all subjects.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏