Python: 综合项目:学生成绩管理(上)

这是 Python 教程的结业项目。我们将用三节课的时间,从零搭建一个完整的学生成绩管理系统。项目分三层:数据层(JSON 持久化)、逻辑层(成绩处理)、展示层(命令行界面)。这一课先做"地基"——需求和数据层。


1. 项目整体规划

TEXT 📖 仅展示
学生成绩管理系统
├── 第34课:数据层 ── 需求分析 + JSON/CSV持久化
├── 第35课:逻辑层 ── 成绩统计 + 排序 + 异常处理
└── 第36课:展示层 ── 菜单界面 + 搜索 + 报表


2. 需求规格

TEXT 📖 仅展示
1. 学生管理
   ├── 添加学生(姓名、学号、班级)
   └── 删除学生(按学号)

2. 成绩管理
   ├── 录入成绩(按学号录入多门功课)
   ├── 修改成绩
   └── 删除成绩记录

3. 统计分析
   ├── 个人成绩单
   ├── 班级排名
   ├── 各科平均分
   └── 分数段分布

4. 数据持久化
   ├── JSON 格式存到文件
   └── 启动时自动加载


3. 数据结构设计

▶ 示例:JSON 数据结构

PYTHON
# 数据以 JSON 格式存储
{
    "students": {
        "2024001": {
            "name": "Alice",
            "class_name": "一班",
            "scores": {
                "语文": 85,
                "数学": 92,
                "英语": 78
            }
        },
        "2024002": {
            "name": "Bob",
            "class_name": "一班",
            "scores": {
                "语文": 90,
                "数学": 88,
                "英语": 95
            }
        }
    },
    "subjects": ["语文", "数学", "英语"],
    "classes": ["一班", "二班"]
}
▶ 试一试

输出:

TEXT 📖 仅展示
# 执行成功
💡 为什么用学号作键? 用学号(字符串)作为字典的键,查找学生就是 O(1) 的——不管有 10 个还是 10000 个学生,查找速度一样快。



4. 数据层实现

▶ 示例:数据层实现与测试

PYTHON 📖 仅展示
import json
import os

DATA_FILE = "grade_system.json"

# ====== 默认数据结构 ======
DEFAULT_DATA = {
    "students": {},
    "subjects": ["语文", "数学", "英语"],
    "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} 已存在!"

    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"已添加学生:{name}({student_id})"


def delete_student(student_id):
    """删除学生"""
    data = load_data()

    if student_id not in data["students"]:
        return False, f"学号 {student_id} 不存在!"

    name = data["students"][student_id]["name"]
    del data["students"][student_id]
    save_data(data)
    return True, f"已删除学生:{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} 不存在!"

    if subject not in data["subjects"]:
        return False, f"科目 {subject} 不存在!"

    if not isinstance(score, (int, float)) or score < 0 or score > 100:
        return False, "成绩必须在 0~100 之间!"

    data["students"][student_id]["scores"][subject] = score
    save_data(data)
    return True, f"已录入 {data['students'][student_id]['name']} 的 {subject} 成绩:{score}分"


def update_score(student_id, subject, 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} 不存在!"

    if subject not in data["students"][student_id]["scores"]:
        return False, f"{data['students'][student_id]['name']} 没有 {subject} 成绩"

    del data["students"][student_id]["scores"][subject]
    save_data(data)
    return True, f"已删除 {data['students'][student_id]['name']} 的 {subject} 成绩"


# ====== 测试代码 ======
if __name__ == "__main__":
    print("=== 测试数据层 ===")
    print(add_student("2024001", "Alice", "一班"))
    print(add_student("2024002", "Bob", "一班"))
    print(add_student("2024003", "Charlie", "二班"))

    print(add_score("2024001", "语文", 85))
    print(add_score("2024001", "数学", 92))
    print(add_score("2024002", "语文", 90))

    student = get_student("2024001")
    print(f"\nAlice的信息:{student}")

    print(f"\n所有学生:{len(get_all_students())} 人")
逻辑代码 86 行(超过 40 行限制,仅展示)

输出:

TEXT 📖 仅展示
=== 测试数据层 ===


▶ 示例:学生数据的基本操作(难度⭐)

PYTHON
# 用字典和列表模拟学生成绩数据操作
students = {}

def add_student(students, sid, name, class_name):
    """添加学生到字典"""
    if sid in students:
        return f"学号 {sid} 已存在!"
    students[sid] = {"name": name, "class_name": class_name, "scores": {}}
    return f"已添加:{name}({sid})"

def add_score(students, sid, subject, score):
    """给学生录入成绩"""
    if sid not in students:
        return f"学号 {sid} 不存在!"
    if score < 0 or score > 100:
        return "成绩必须在 0~100 之间!"
    students[sid]["scores"][subject] = score
    return f"已录入 {students[sid]['name']} 的 {subject}:{score}分"

def get_average(students, sid):
    """计算某学生的平均分"""
    if sid not in students or not students[sid]["scores"]:
        return None
    scores = students[sid]["scores"].values()
    return sum(scores) / len(scores)

# 测试
print(add_student(students, "001", "Alice", "一班"))
print(add_student(students, "002", "Bob", "一班"))
print(add_score(students, "001", "语文", 85))
print(add_score(students, "001", "数学", 92))
print(add_score(students, "002", "语文", 78))

avg = get_average(students, "001")
print(f"\nAlice 平均分:{avg:.1f}")
print(f"Bob 语文成绩:{students['002']['scores']['语文']}分")
print(f"全班学生数:{len(students)}")
▶ 试一试

输出:

TEXT 📖 仅展示
已添加:Alice(001)
已添加:Bob(一班)
已录入 Alice 的 语文:85分
已录入 Alice 的 数学:92分
已录入 Bob 的 语文:78分

Alice 平均分:88.5
Bob 语文成绩:78分
全班学生数:2


5. 设计要点


❓ 常见问题

Q 为什么用学号做字典键而不是姓名?
A 姓名可能重复(同名的学生),学号是唯一标识。字典键必须唯一,用学号能保证 O(1) 的查找速度且不会冲突。
Q JSON 文件路径写死在代码里好吗?
A 教程中为了简洁直接写死路径。实际项目中应该用配置文件或命令行参数指定路径,可以用 os.path.join() 拼接路径,避免硬编码。
Q 为什么要返回 (success, message) 元组而不是直接抛异常?
A 这是"状态码模式"——调用方可以根据 success 决定怎么处理,比 try/except 更轻量。适合这种命令行小工具,大项目更推荐用自定义异常类。

📖 小节


📝 作业

  1. 基础题(难度⭐):运行上面的数据层代码,添加 2~3 个学生和他们的成绩,检查 JSON 文件是否正确生成。

  2. 进阶题(难度⭐⭐):给数据层增加一个 update_student(student_id, name=None, class_name=None) 函数,只更新提供的字段(其他字段保持不变)。

  3. 挑战题(难度⭐⭐⭐):给 add_score 增加批量录入功能——接收一个字典 {"语文": 85, "数学": 92},一次性录入多门成绩。要求:如果其中一门成绩无效,整批操作回滚(不保存任何数据)。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

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

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