第3章 3.5 综合实战:学生成绩管理系统
🎯 开场:为什么要学这个?
上一章我们学会了 Set/Map/WeakMap 这些「高级盒子」,知道了它们比普通数组对象强在哪。但光说不练假把式——你有没有遇到过这种情况:
- 班主任让你整理全班成绩,要查重(不能重复录入同一个学生)、要快速查找某人的成绩、要按成绩排序……
- 用 Excel 人工操作?复制粘贴半天,最后还容易搞错行
- 网上下载的成绩单格式乱七八糟,逗号、空格、中英文全混在一起……
学完这一章,你能做出一个自己的「学生成绩管理小工具」,输入数据就能自动去重、查询、统计、导出。班主任看了都问你「哪儿下的软件」。
🧱 基础:核心概念(小白视角)
什么是 CRUD?
CRUD 是四个英文单词的缩写,代表对数据的基本操作:
- Create(创建)—— 添加新学生成绩
- Read(读取)—— 查看某个学生或全班成绩
- Update(更新)—— 修改某个学生的成绩
- Delete(删除\n\n
\n\n
\n\n)—— 删除某个学生的记录
你可以把它理解成对着一本通讯录干活:写新号码(Create)、翻看某人的号(Read)、帮人换号(Update)、把某人划掉(Delete)。整个学生成绩管理系统,就是围绕这四个操作展开的。
为什么用 Set 来管理学生名单?
普通数组有个问题:可能有重复。如果不小心两次录入「张小明」,数组里就有两条记录,查的时候容易混淆。
Set 就像一个自动去重的袋子——你往里扔东西,它会自动忽略重复的。举例:
# 模拟学生名单
students = set()
students.add("张小明") # 加进去
students.add("张小明") # 再加一次试试?
students.add("李华")
print(students) # 输出:{'张小明', '李华'} 只有两条,重复的被忽略了
为什么用类来组织代码?
如果你有 50 个学生,每个学生有「姓名、语文成绩、数学成绩」三个信息……你是想写 50 组散变量,还是想写一个模板统一管理?
类就像做饼干的模具。你定义好模具(类),就能批量生产饼干(对象),每个饼干长得一样,但内容独立。
class Student:
def __init__(self, name, chinese, math):
self.name = name
self.chinese = chinese
self.math = math
def total_score(self):
"""计算总分"""
return self.chinese + self.math
def average_score(self):
"""计算平均分"""
return (self.chinese + self.math) / 2
# 用模板创建两个学生对象
student1 = Student("张小明", 85, 92)
student2 = Student("李华", 78, 88)
print(student1.total_score()) # 输出:177
print(student2.average_score()) # 输出:83.0
用 Map 来存成绩数据
如果说 Set 是「去重袋子」,那 Map 就是「标签盒子」——每个数据都有一个唯一的「钥匙」对应。
这里我们用 name(姓名)作为钥匙,Student 对象作为值,存入 Map:
from collections import Map
# 初始化一个 Map(字典),存所有学生成绩
# 说白了就是:人名 -> 学生对象 的对应表
score_map = {
"张小明": Student("张小明", 85, 92),
"李华": Student("李华", 78, 88),
"王芳": Student("王芳", 90, 85)
}
# 查一下张三的成绩
print(score_map["张小明"].total_score()) # 输出:177
Set 和 Map 的配合:自动去重 + 快速查找
想象一下:Set 存「有哪些学生」(去重名单),Map 存「每个学生的成绩详情」。查找时先问 Set「有没有这个人」,再从 Map 里拿成绩——这就是最常见的组合用法。
# 用 Set 存学生名单(自动去重)
name_set = set()
# 用 Map 存学生对象
score_map = {}
def add_student(name, chinese, math):
if name in name_set:
print(f"⚠️ {name} 已经存在,无需重复添加")
return False
name_set.add(name)
score_map[name] = Student(name, chinese, math)
print(f"✅ {name} 添加成功")
return True
add_student("张小明", 85, 92) # ✅ 张小明 添加成功
add_student("张小明", 85, 92) # ⚠️ 张小明 已经存在,无需重复添加
🔥 实战:3 个递进的小项目
项目 1:5 分钟搞定基础 CRUD(跟着抄就能跑)
这个项目实现最基本的功能:增删查改。代码不长,但五脏俱全。
from collections import defaultdict
class StudentManager:
def __init__(self):
# 用 defaultdict,省去判断键是否存在的心智负担
self.students = {}
self.names = set() # 用 Set 存名单,自动去重
def add(self, name, chinese, math):
"""添加学生成绩"""
if name in self.names:
print(f"⚠️ {name} 已存在")
return False
self.names.add(name)
self.students[name] = {"chinese": chinese, "math": math}
print(f"✅ {name} 添加成功")
return True
def search(self, name):
"""查找学生成绩"""
if name not in self.names:
print(f"❌ {name} 不存在")
return None
scores = self.students[name]
total = scores["chinese"] + scores["math"]
print(f"{name}:语文={scores['chinese']},数学={scores['math']},总分={total}")
return scores
def update(self, name, chinese=None, math=None):
"""更新成绩"""
if name not in self.names:
print(f"❌ {name} 不存在,无法更新")
return False
if chinese is not None:
self.students[name]["chinese"] = chinese
if math is not None:
self.students[name]["math"] = math
print(f"✅ {name} 更新成功")
return True
def delete(self, name):
"""删除学生"""
if name not in self.names:
print(f"❌ {name} 不存在,无法删除")
return False
self.names.remove(name)
del self.students[name]
print(f"✅ {name} 删除成功")
return True
def show_all(self):
"""显示所有学生"""
if not self.names:
print("📭 暂无学生数据")
return
print("=" * 40)
print(f"共 {len(self.names)} 名学生")
print("=" * 40)
for name in self.names:
scores = self.students[name]
total = scores["chinese"] + scores["math"]
avg = total / 2
print(f"{name}:语文={scores['chinese']},数学={scores['math']},总分={total},均分={avg:.1f}")
# ========== 运行测试 ==========
manager = StudentManager()
manager.add("张小明", 85, 92)
manager.add("李华", 78, 88)
manager.add("王芳", 90, 85)
manager.add("张小明", 85, 92) # 重复添加测试
print()
manager.search("李华")
print()
manager.update("李华", chinese=82) # 只改语文
print()
manager.show_all()
预期输出:
✅ 张小明 添加成功
✅ 李华 添加成功
✅ 王芳 添加成功
⚠️ 张小明 已存在
李华:语文=78,数学=88,总分=166
✅ 李华 更新成功
========================================
共 3 名学生
========================================
张小明:语文=85,数学=92,总分=177,均分=88.5
李华:语文=82,数学=88,总分=170,均分=85.0
王芳:语文=90,数学=85,总分=175,均分=87.5
一句话解释:用 set 存名单去重,dict 存成绩数据,这就是最简单的「名单本 + 档案袋」模式。
项目 2:从 CSV 文件批量导入成绩
真实工作中,数据往往不在代码里,而是存在文件或网上。这一节我们从 CSV 导入数据,顺便做个格式清洗。
假设有一个 scores.csv 文件,内容如下:
姓名,语文,数学
张小明,85,92
李华,78,88
王芳,90,85
赵强,72,95
孙丽,88,78
import csv
from collections import defaultdict
class StudentManager:
def __init__(self):
self.students = {}
self.names = set()
def add(self, name, chinese, math):
if name in self.names:
return False
self.names.add(name)
self.students[name] = {"chinese": chinese, "math": math}
return True
def import_from_csv(self, filepath):
"""从 CSV 文件导入成绩"""
imported = 0
skipped = 0
errors = 0
with open(filepath, "r", encoding="utf-8") as f:
reader = csv.DictReader(f) # 读取成字典列表
for row in reader:
try:
name = row["姓名"].strip() # 去掉空格
chinese = int(row["语文"].strip())
math = int(row["数学"].strip())
if self.add(name, chinese, math):
imported += 1
else:
skipped += 1
except (ValueError, KeyError) as e:
errors += 1
print(f"⚠️ 数据格式错误:{row},原因:{e}")
print(f"📥 导入完成:新增 {imported} 条,跳过 {skipped} 条重复,错误 {errors} 条")
return imported, skipped, errors
def show_all(self):
if not self.names:
print("📭 暂无数据")
return
print("-" * 45)
print(f"{'姓名':^8} | {'语文':^5} | {'数学':^5} | {'总分':^5} | {'均分':^5}")
print("-" * 45)
for name in sorted(self.names): # 按姓名排序输出
s = self.students[name]
total = s["chinese"] + s["math"]
avg = total / 2
print(f"{name:^8} | {s['chinese']:^5} | {s['math']:^5} | {total:^5} | {avg:^5.1f}")
print("-" * 45)
print(f"共 {len(self.names)} 人,平均分 {self.statistics()['class_avg']:.1f}")
def statistics(self):
"""班级统计"""
if not self.students:
return {"class_avg": 0, "highest": None, "lowest": None}
totals = [s["chinese"] + s["math"] for s in self.students.values()]
avg = sum(totals) / len(totals)
highest_name = max(self.students, key=lambda n: sum(self.students[n].values()))
lowest_name = min(self.students, key=lambda n: sum(self.students[n].values()))
return {
"class_avg": avg,
"highest": (highest_name, sum(self.students[highest_name].values())),
"lowest": (lowest_name, sum(self.students[lowest_name].values()))
}
# ========== 运行测试 ==========
# 先创建测试文件
with open("scores.csv", "w", encoding="utf-8") as f:
f.write("姓名,语文,数学\n张小明,85,92\n李华,78,88\n王芳,90,85\n赵强,72,95\n孙丽,88,78\n")
manager = StudentManager()
manager.import_from_csv("scores.csv")
manager.show_all()
stats = manager.statistics()
print(f"\n🏆 班级最高:{stats['highest'][0]}({stats['highest'][1]}分)")
print(f"📉 班级最低:{stats['lowest'][0]}({stats['lowest'][1]}分)")
预期输出:
📥 导入完成:新增 5 条,跳过 0 条重复,错误 0 条
---------------------------------------------
姓名 | 语文 | 数学 | 总分 | 均分
---------------------------------------------
王芳 | 90 | 85 | 175 | 87.5
张小明 | 85 | 92 | 177 | 88.5
李华 | 78 | 88 | 166 | 83.0
赵强 | 72 | 95 | 167 | 83.5
孙丽 | 88 | 78 | 166 | 83.0
---------------------------------------------
共 5 人,平均分 170.2
🏆 班级最高:张小明(177分)
📉 班级最低:李华(166分)
一句话解释:csv.DictReader 帮你按列名读取,不用数下标;strip() 去掉多余空格;try/except 捕获格式错误让程序不崩溃。
项目 3:做一个带交互的命令行小工具
把前两个项目的功能串起来,做一个真实可用的小工具:输入学号选择操作,支持批量导入、查询、导出报告。
import csv
from collections import defaultdict
class StudentManager:
def __init__(self):
self.students = {}
self.names = set()
def add(self, name, chinese, math):
if name in self.names:
return False
self.names.add(name)
self.students[name] = {"chinese": chinese, "math": math}
return True
def import_from_csv(self, filepath):
imported = skipped = errors = 0
try:
with open(filepath, "r", encoding="utf-8") as f:
for row in csv.DictReader(f):
try:
name = row["姓名"].strip()
chinese = int(row["语文"].strip())
math = int(row["数学"].strip())
if self.add(name, chinese, math):
imported += 1
else:
skipped += 1
except (ValueError, KeyError):
errors += 1
except FileNotFoundError:
print(f"❌ 文件 {filepath} 不存在")
return 0, 0, 1
print(f"📥 导入完成:新增{imported},重复{skipped},错误{errors}")
return imported, skipped, errors
def export_to_csv(self, filepath):
"""导出成绩到 CSV"""
with open(filepath, "w", encoding="utf-8", newline="") as f:
writer = csv.writer(f)
writer.writerow(["姓名", "语文", "数学", "总分", "均分"])
for name in sorted(self.names):
s = self.students[name]
total = s["chinese"] + s["math"]
avg = total / 2
writer.writerow([name, s["chinese"], s["math"], total, f"{avg:.1f}"])
print(f"💾 已导出到 {filepath}")
def search(self, name):
if name not in self.names:
return None
s = self.students[name]
return {"name": name, **s, "total": s["chinese"] + s["math"], "avg": (s["chinese"] + s["math"]) / 2}
def rank(self):
"""按总分排名"""
ranked = []
for name in self.names:
s = self.students[name]
ranked.append((name, s["chinese"] + s["math"]))
ranked.sort(key=lambda x: x[1], reverse=True)
return ranked
def run_interactive(self):
"""交互式运行"""
print("\n" + "=" * 40)
print(" 🎓 学生成绩管理系统 v1.0")
print("=" * 40)
print(" 1. 导入 CSV 文件")
print(" 2. 查找学生")
print(" 3. 显示排名")
print(" 4. 导出报告")
print(" 0. 退出")
print("-" * 40)
while True:
choice = input("\n请选择操作(0-4):").strip()
if choice == "1":
path = input("请输入 CSV 文件路径:").strip()
self.import_from_csv(path)
elif choice == "2":
name = input("请输入学生姓名:").strip()
result = self.search(name)
if result:
print(f"\n📋 {result['name']} 的成绩:")
print(f" 语文:{result['chinese']}")
print(f" 数学:{result['math']}")
print(f" 总分:{result['total']}")
print(f" 均分:{result['avg']:.1f}")
else:
print(f"❌ 未找到学生:{name}")
elif choice == "3":
if not self.names:
print("📭 暂无数据,请先导入")
continue
ranked = self.rank()
print("\n🏆 总分排名:")
print("-" * 30)
for i, (name, score) in enumerate(ranked, 1):
medal = "🥇" if i == 1 else "🥈" if i == 2 else "🥉" if i == 3 else f" {i} "
print(f" {medal} {name}:{score}分")
elif choice == "4":
if not self.names:
print("📭 暂无数据")
continue
path = input("请输入导出文件名(如 result.csv):").strip()
self.export_to_csv(path)
elif choice == "0":
print("👋 再见!")
break
else:
print("⚠️ 无效选项,请重试")
# ========== 运行 ==========
# 创建测试数据
with open("scores.csv", "w", encoding="utf-8") as f:
f.write("姓名,语文,数学\n张小明,85,92\n李华,78,88\n王芳,90,85\n赵强,72,95\n孙丽,88,78\n")
manager = StudentManager()
manager.run_interactive()
预期输出(交互流程示例):
========================================
🎓 学生成绩管理系统 v1.0
========================================
1. 导入 CSV 文件
2. 查找学生
3. 显示排名
4. 导出报告
0. 退出
----------------------------------------
请选择操作(0-4):1
请输入 CSV 文件路径:scores.csv
📥 导入完成:新增5,重复0,错误0
请选择操作(0-4):2
请输入学生姓名:张小明
📋 张小明 的成绩:
文:85
学:92
分:177
分:88.5
请选择操作(0-4):3
🏆 总分排名:
------------------------------
🥇 张小明:177分
🥈 王芳:175分
🥉 赵强:167分
李华:166分
孙丽:166分
请选择操作(0-4):4
请输入导出文件名(如 result.csv):result.csv
💾 已导出到 result.csv
请选择操作(0-4):0
👋 再见!
一句话解释:把增删改查封装成选项菜单,数据来自 CSV、输出到 CSV——这就是一个可以真正交给别人用的小工具。
💪 进阶:常见坑 + 性能小贴士
坑 1:字典的键不存在时直接报错
# ❌ 错误写法
scores = {"张三": 85}
print(scores["李四"]) # KeyError: '李四'
# ✅ 正确写法:用 get() 并给默认值
print(scores.get("李四", 0)) # 输出:0,没找到就返回默认值
坑 2:Set 去重后只保留一个,但你想保留最新的
# ❌ 错误写法:Set 只保留第一个,后面的被忽略
names = set()
names.add("张三")
names.add("张三") # 被静默忽略,不会报错
# ✅ 如果你需要保留最新的,用 dict 代替
names = {}
names["张三"] = 85
names["张三"] = 90 # 覆盖之前的值
print(names) # {'张三': 90}
坑 3:CSV 中文编码问题
# ❌ 错误写法:没有指定编码,中文系统可能乱码
with open("scores.csv", "r") as f:
...
# ✅ 正确写法:明确指定 utf-8
with open("scores.csv", "r", encoding="utf-8") as f:
...
坑 4:修改字典时「遍历中修改」导致迭代器失效
# ❌ 错误写法:遍历时删除元素可能出问题
for name in list(manager.names): # list() 先复制一份
if manager.students[name]["chinese"] < 60:
manager.delete(name)
# ✅ 或者先记录要删的,再删
to_delete = [name for name in manager.names if manager.students[name]["chinese"] < 60]
for name in to_delete:
manager.delete(name)
坑 5:浮点数精度导致比较出错
# ❌ 错误写法:用 == 比较浮点数
avg = (85 + 92) / 2
print(avg == 88.5) # 可能输出 False(浮点精度问题)
# ✅ 正确写法:设定一个误差范围
print(abs(avg - 88.5) < 0.0001) # True
性能小贴士:批量操作减少文件 IO
如果需要频繁读写 CSV,不要每次操作都打开关闭文件——可以先把数据全部加载到内存(dict/set),程序运行结束后统一写入。
# 把所有数据准备好,一次性写入
with open("output.csv", "w", encoding="utf-8", newline="") as f:
writer = csv.writer(f)
writer.writerow(["姓名", "语文", "数学"])
for name, scores in manager.students.items():
writer.writerow([name, scores["chinese"], scores["math"]])
调试技巧:用 print 定位问题
def add(self, name, chinese, math):
print(f"[DEBUG] 准备添加:name={name}, chinese={chinese}, math={math}") # 打印入参
if name in self.names:
print(f"[DEBUG] {name} 已存在,跳过")
return False
self.names.add(name)
self.students[name] = {"chinese": chinese, "math": math}
print(f"[DEBUG] 添加成功,当前共 {len(self.names)} 人")
return True
✏️ 练习题
练习 1(2 分钟):添加新学生
- 输入:调用 add("刘洋", 80, 86)
- 预期输出:✅ 刘洋 添加成功
- 提示:复用项目 1 的 StudentManager 类
练习 2(2 分钟):添加去重测试
- 输入:连续两次调用 add("刘洋", 80, 86)
- 预期输出:第一次 ✅,第二次 ⚠️ 刘洋 已存在
- 提示:利用 Set 自动去重的特性
练习 3(3 分钟):处理新数据文件
- 输入:创建一个新的 CSV 文件 new_scores.csv,包含 3 个学生,调用 import_from_csv()
- 预期输出:显示「新增 3 条」
- 提示:格式必须是 姓名,语文,数学
练习 4(5 分钟):串起导入和排名
- 输入:先用 import_from_csv() 导入数据,再调用 rank() 查看排名
- 预期输出:按总分从高到低排序的名单
- 提示:参考项目 3 的 rank() 方法
练习 5(5 分钟):看报错修 Bug
- 输入:以下代码运行后报 KeyError,找出原因并修复
scores = {"张三": 85}
print(scores["李四"])
- 预期输出:修复后输出
0或不存在 - 提示:用
.get()方法并设置默认值
作业:做一个「班级成绩分析工具」
- 需求描述:读取一个包含学生成绩的 CSV 文件,输出班级统计分析报告
- 功能点:
1. 从 CSV 文件导入成绩(支持中文列名)
2. 显示班级平均分、最高分、最低分
3. 找出不及格学生(任意一科 < 60)
4. 按总分排名输出前 3 名 - 加分项:
1. 支持从命令行接收文件路径(如python main.py scores.csv)
2. 输出格式美化(用 emoji、对齐) - 验收标准:能跑起来 + 有注释 + 输出符合预期
- 提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
本文学了 3 件事:
1. 用 Set + Map/dict 配合实现去重 + 快速查找
2. 用 类 封装数据和操作,做成可复用的工具
3. 用 CSV 文件 做数据导入导出,让程序和真实世界连接
延伸学习资源:
- Python 官方文档:collections 模块—— defaultdict、OrderedDict 等更多数据结构
- 《Python 编程:从入门到实践》—— 第 9 章「类」,有更完整的面向对象讲解
- Real Python - Python CSV 教程—— CSV 处理进阶技巧
互动钩子:
你在工作中需要处理「名单去重」或「成绩统计」的场景吗?用什么工具处理的?评论区聊聊,帮你出主意!
下一章我们要进入一个全新主题——异步编程。你有没有遇到过这种情况:程序要等一个很慢的操作(比如下载文件、请求 API),然后整个界面就卡住了?下一章教你怎么让程序「边等边干别的」!

评论(0)