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())} 人")
输出:
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. 设计要点
- 函数返回值统一:每个函数返回
(bool, str)元组——True表示成功,False表示失败,字符串是提示信息。 - 重复操作封装:
load_data()和save_data()两个函数复用,所有数据操作都通过它们。 - 输入验证:成绩范围检查、学号存在性检查在数据层完成,上层调用者不用重复写。
- 批量操作准备:后续第 35、36 课直接在数据层基础上加功能。
❓ 常见问题
Q 为什么用学号做字典键而不是姓名?
A 姓名可能重复(同名的学生),学号是唯一标识。字典键必须唯一,用学号能保证 O(1) 的查找速度且不会冲突。
Q JSON 文件路径写死在代码里好吗?
A 教程中为了简洁直接写死路径。实际项目中应该用配置文件或命令行参数指定路径,可以用
os.path.join() 拼接路径,避免硬编码。Q 为什么要返回
(success, message) 元组而不是直接抛异常?A 这是"状态码模式"——调用方可以根据
success 决定怎么处理,比 try/except 更轻量。适合这种命令行小工具,大项目更推荐用自定义异常类。📖 小节
- 项目分三层架构:数据层 → 逻辑层 → 展示层,本节课完成数据层
- 数据用 JSON 格式持久化,学号作为字典键实现 O(1) 查找
- 成绩范围 0~100,录入时自动验证
- 所有操作函数返回
(success, message)元组,便于调用方处理
📝 作业
-
基础题(难度⭐):运行上面的数据层代码,添加 2~3 个学生和他们的成绩,检查 JSON 文件是否正确生成。
-
进阶题(难度⭐⭐):给数据层增加一个
update_student(student_id, name=None, class_name=None)函数,只更新提供的字段(其他字段保持不变)。 -
挑战题(难度⭐⭐⭐):给
add_score增加批量录入功能——接收一个字典{"语文": 85, "数学": 92},一次性录入多门成绩。要求:如果其中一门成绩无效,整批操作回滚(不保存任何数据)。