第3章 3.5 综合实战:学生成绩管理系统

🎯 开场:为什么要学这个?

上一章我们学会了 Set/Map/WeakMap 这些「高级盒子」,知道了它们比普通数组对象强在哪。但光说不练假把式——你有没有遇到过这种情况:

  • 班主任让你整理全班成绩,要查重(不能重复录入同一个学生)、要快速查找某人的成绩、要按成绩排序……
  • 用 Excel 人工操作?复制粘贴半天,最后还容易搞错行
  • 网上下载的成绩单格式乱七八糟,逗号、空格、中英文全混在一起……

学完这一章,你能做出一个自己的「学生成绩管理小工具」,输入数据就能自动去重、查询、统计、导出。班主任看了都问你「哪儿下的软件」。


🧱 基础:核心概念(小白视角)

什么是 CRUD?

CRUD 是四个英文单词的缩写,代表对数据的基本操作:

  • Create(创建)—— 添加新学生成绩
  • Read(读取)—— 查看某个学生或全班成绩
  • Update(更新)—— 修改某个学生的成绩
  • Delete(删除\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\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),然后整个界面就卡住了?下一章教你怎么让程序「边等边干别的」!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。