学生成绩管理系统这个题目,第一次带 Python 入门班的人应该都绕不过。有人嫌它老套,我却觉得,这是把零散语法串成完整逻辑链的最合适载体:变量、列表、字典、循环、分支、函数、异常处理,这些入门阶段绕不开的核心知识点,恰好都能在这一个项目里找到落脚点。所以我在规划“Python 系列教案”的时候,故意把这一题放到第一篇,用最朴素的方式把它讲透。这篇就是整套教案里的第一课,目标是做一个不依赖任何第三方库、纯命令行交互的学生成绩管理系统(基础版),它能完成成绩的录入、查询、修改、删除、统计和排序。适合刚学完 Python 基础语法、想找一个完整项目练手的人,也适合拿它作为备课素材的新手教师。文章里我会把每一步选择的理由也写出来,而不只是给代码。
1. 项目整体设计:代码没写一行,先把思路理顺
1.1 基础版的目标与定位
一个“学生成绩管理系统”听起来很宽泛,真要做可以做成 Web 系统、桌面软件、带数据库,甚至加图表可视化。但作为系列教案的第一篇,必须克制。基础版的目标不是功能齐全,而是让学习者用最少的语法知识,走通一个完整项目从设计到落地的全过程。
这一版我确定的技术边界很简单:不用类、不用数据库、不用第三方库,只靠 Python 的内置数据结构——列表、字典,再加函数、循环、条件判断来完成。所有数据临时存在内存里,程序退出后数据不保留。这个限制不是偷懒,而是刻意为之:把复杂度压到最低,学习者才能把注意力放在程序本身的逻辑流程上,而不是被框架、依赖和配置分散精力。
对应的,它的能力边界也清晰:能录入学生成绩、能显示所有学生、能按学号或姓名查询、能修改成绩、能删除学生、能统计平均分和及格率、能按总分排序。将来的第二篇、第三篇可以在此基础上加入文件存储、异常处理体系、面向对象重构、数据库,一步一步把软件工程的思想引进来。
1.2 功能模块拆解与学习地图
拿到这个题目,第一步不是打开编辑器敲代码,而是画功能清单。我给这套教案设计的交互方式是命令行菜单,程序启动后循环显示菜单,用户输入数字选择对应功能,直到输入退出指令才结束。
我的菜单设计如下:
- 输入 1:录入学生成绩
- 输入 2:显示全部学生
- 输入 3:按学号或姓名查询学生
- 输入 4:修改学生成绩
- 输入 5:删除学生
- 输入 6:统计成绩(平均分、最高分、最低分、及格率)
- 输入 7:按总分排序显示
- 输入 0:退出系统
这八个功能看起来是一口气列出来的,但它们对应的知识点分布在 Python 入门的不同章节里,正好串成一条学习地图。录入功能用到输入输出、类型转换、循环校验,显示功能用到 for 循环、格式化字符串,查询功能用到分支判断和循环里的 break,修改和删除功能用到列表、字典元素的增删改,统计功能用到 sum、len、max、min 这些内置函数,排序功能用到 sorted 和 key 参数。
这些知识点单独学的时候都很简单,但组合在一个项目里,难点就变成了“怎么把逻辑串起来”。所以我建议带学生或者自学者在动手之前,先对着菜单想象一遍:每按一个数字,程序内部要经历哪些步骤?这样在写代码的时候就不会晕。
1.3 数据结构选型:为什么是列表套字典
数据存储结构是整个项目的地基,许多人一上来就用一个二维列表存学号、姓名、成绩,类似这样:
students = [ ["001", "张三", 88, 90, 92], ["002", "李四", 77, 85, 81], ]这种方案不是不能跑,但它有一个明显的问题:访问“张三的英语成绩”要先找到所在的行,再记住英语在第几列。如果将来调换来科目的顺序,所有下标都要跟着改,代码可读性很差。在数据量小的时候无所谓,一旦字段增加,这种结构会让人崩溃。
所以我选择用“列表套字典”的结构:每个学生是一个字典,字典里用键名直接表达字段含义,然后把这些字典统一放进一个列表里。
students = [ {"name": "张三", "student_id": "001", "scores": {"语文": 88, "数学": 90, "英语": 92}}, {"name": "李四", "student_id": "002", "scores": {"语文": 77, "数学": 85, "英语": 81}}, ]这样的好处非常直观:students[0] 是一个学生,students[0]["name"] 拿到姓名,students[0]["scores"]["数学"] 拿到数学成绩。语义完全自解释,后续的读、改、删都围绕键名操作,根本不用关心字段顺序。成绩这里我再嵌套了一层字典,是为了让科目和分数一一对应,科目名本身就是键,成绩就是值,这样统计遍历的时候只需要取 .values() 或者用科目名去取。基础版里科目固定为语文、数学、英语三科,但结构上已经为扩科留了余地。
2. 开发环境准备:别在安装这一步就把自己劝退
2.1 安装 Python 并加入系统 PATH
这个项目只需要 Python 标准库,不需要额外安装第三方包,所以环境准备的复杂度已经很低了。但很多新手倒在最开始的安装环节,我在这里把标准流程走一遍。
去 python.org 官网下载安装包,选最新稳定版即可,目前主流的 3.10 到 3.12 版本都行。老版本不用纠结,新版本在语法层面完全兼容这个项目用到的内容。安装时有一个极其关键的勾选项:Add python.exe to PATH,一定要勾上。这一步的作用是告诉系统,你可以在命令行的任何目录下直接输入 python 命令来启动解释器。很多人在安装时忽略这个选项,后面跑 python 命令永远提示“不是内部或外部命令”,问题就出在这一步。如果安装时忘记了,也可以事后手动把 Python 安装路径加入环境变量,但没必要绕这个弯。
安装完成后,打开命令行窗口,输入python --version,能正常打印出类似Python 3.12.x的信息,就说明安装成功了。
2.2 VSCode 配置 Python 开发环境
编辑器方面,新手最推荐 VSCode,轻量、插件生态好,配置 Python 开发环境三步就能完成。
第一步,在 VSCode 扩展商店里搜索“Python”,安装微软官方发布的那个插件,扩展名是 Python,发布者是 Microsoft。它负责代码高亮、智能提示、代码补全、调试支持。第二步,打开任意一个 .py 文件,按 Ctrl+Shift+P 调出命令面板,输入 “Python: Select Interpreter”,选择刚才安装的 Python 解释器路径。第三步,有必要的话可以调整控制台终端的编码,这个项目里全部是中文交互,Windows 终端有时会遇到编码问题,后面第 4 节我会专门讲。
配置好之后,直接点右上角的三角形运行按钮,就可以在当前窗口终端里看到程序的运行结果。这里多说一句:VSCode 里运行 Python 程序时,界面下方会弹出 Python 终端面板,建议初学者养成“先看输出,再回代码”的习惯,写项目的过程本质上就是调输出的过程。
2.3 最小验证:先跑通一个测试脚本
很多人上来就写几十行功能代码,出了错不知道是环境问题还是代码问题。我的习惯是,环境配好后先跑一个最不容易出错的最小脚本,确认整条运行链路通畅,再开始正式写项目。
新建一个文件 test.py,输入:
print("Python 环境正常,开始学习学生成绩管理系统!") name = input("请输入你的名字:") print(f"你好,{name}!")运行后如果能正常打印并接收输入,环境就没有任何问题。这一步虽然简单,但把“环境变量是否配置正确”“解释器是否选对”“终端编码是否正常”这些潜在问题一次性暴露出来,后面写大段代码的时候就能少很多无谓的折腾。
3. 核心功能实现:代码逐段拆解
3.1 初始化数据与主菜单循环
先看程序入口部分。我习惯把学生数据的初始值写成一个全局列表,方便在没有任何录入操作的情况下先有两条示例数据可看,也方便调试。
students = [ {"name": "张三", "student_id": "001", "scores": {"语文": 88, "数学": 90, "英语": 92}}, {"name": "李四", "student_id": "002", "scores": {"语文": 77, "数学": 85, "英语": 81}}, ] def main(): while True: print("\n===== 学生成绩管理系统(基础版) =====") print("1. 录入学生成绩") print("2. 显示全部学生") print("3. 按学号或姓名查询学生") print("4. 修改学生成绩") print("5. 删除学生") print("6. 统计成绩") print("7. 按总分排序显示") print("0. 退出系统") choice = input("请输入操作编号:") if choice == "1": add_student() elif choice == "2": show_all_students() elif choice == "3": search_student() elif choice == "4": modify_score() elif choice == "5": delete_student() elif choice == "6": show_statistics() elif choice == "7": sort_students_by_total() elif choice == "0": print("感谢使用,再见!") break else: print("无效的选项,请输入 0-7 之间的数字。") if __name__ == "__main__": main()主程序采用 while True 无限循环,这是控制台菜单类程序的经典写法。用户每次只输入一个数字,程序用 if-elif 分支判断跳转到对应的函数。之所以不直接用 for 循环,是因为我们无法预知用户会操作多少次,也不希望在每次操作后让程序自动退出,循环体的结束条件只能是用户主动输入“0”然后 break。
这里有一个至关重要的设计点,也是初学者很容易困惑的地方:各个功能都被封装成了独立函数,整个 main 流程非常干净。你在 main 里看到的只是“按条件调函数”,而不是几十行稠密的业务逻辑。这既是为了阅读清晰,也是为后续维护打基础。将来想加一个“导出 CSV”的功能,就是多一个函数再加一个 elif 分支的事,不用动其他代码。
3.2 录入学生信息:类型转换才是最大的坑
第一个功能是录入学生成绩。刚接触编码的人最容易在“怎么处理用户输入”这件事上翻车,这一块的代码也最能体现防御性编程的思想。
def add_student(): name = input("请输入学生姓名:").strip() if not name: print("姓名不能为空!") return student_id = input("请输入学号:").strip() # 检查学号是否重复 for stu in students: if stu["student_id"] == student_id: print("该学号已存在,不能重复录入!") return scores = {} for course in ["语文", "数学", "英语"]: while True: try: score = float(input(f"请输入{course}成绩:")) if 0 <= score <= 100: scores[course] = score break else: print("成绩必须在 0 到 100 之间,请重新输入。") except ValueError: print("输入无效,成绩必须是数字,请重新输入。") students.append({"name": name, "student_id": student_id, "scores": scores}) print(f"学生 {name}(学号 {student_id})的成绩录入成功!")这个函数看起来简单,实际藏着三个关键点。
学号唯一性校验:在向列表追加数据之前,先用 for 循环遍历已有学生,比对 student_id 字段是否重复,一旦发现重复就立刻 return,函数不再继续执行。这种“先校验后写入”的顺序,任何项目里都用得上。
成绩合法性校验:我在浮点数转换外面套了 try-except,捕获 ValueError。用户要是输入了“abc”或者留空,float() 会直接抛异常,不处理的话程序就崩了。加上异常处理后,程序会提示输入无效,并让用户重新输入。紧跟着再来一层范围判断,控制在 0 到 100 之间。很多初版代码只写score = float(input()),一输入非数字就报错退出,那才是项目体验最差的地方。
录入顺序也值得琢磨:先收集 name 和 student_id,再单独建一个 scores 空字典装成绩,最后一次性把新学生作为一个字典 append 到全局列表里。这样在录入过程中如果中途出了问题,students 列表不会被污染,仍然保持之前的数据。
3.3 查询与修改学生信息
查询功能在整个系统里是“承上启下”的一环,修改、删除都需要先定位到学生。我把它封装成一个公共函数,返回查到的学生字典,而不是直接在函数里打印结果。
def find_student(keyword): for stu in students: if stu["student_id"] == keyword or stu["name"] == keyword: return stu return None def search_student(): keyword = input("请输入要查询的学号或姓名:").strip() stu = find_student(keyword) if stu: print(f"姓名:{stu['name']},学号:{stu['student_id']}") for course, score in stu["scores"].items(): print(f"{course}:{score}") else: print("没有找到对应的学生。")这里为什么要单独返回一个 stu 对象?因为后续的修改和删除功能都要复用这个“按关键字找学生”的逻辑。如果查询功能里写死打印输出,修改功能又重写一遍查找逻辑,代码就会重复且难以维护。把查找抽出来,三个功能就只维护一份查找代码,这就是函数复用的意义。
find_student 返回 None 的处理也值得讲清楚:在 Python 里,函数如果没有显式 return,默认返回 None。我允许调用方拿到 None 后自行判断怎么处理,而不是在 find_student 内部打印“没有找到”,这样查找函数只负责查找,提示信息交给调用方决定,职责更单一。
修改成绩就是在 find 的基础上再往下走一层:
def modify_score(): keyword = input("请输入要修改的学生学号或姓名:").strip() stu = find_student(keyword) if not stu: print("没有找到对应的学生。") return print("当前成绩:") for course, score in stu["scores"].items(): print(f"{course}:{score}") course = input("请输入要修改的科目名称(语文/数学/英语):").strip() if course not in stu["scores"]: print("科目不存在,无法修改。") return while True: try: new_score = float(input(f"请输入新的{course}成绩:")) if 0 <= new_score <= 100: stu["scores"][course] = new_score print("修改成功!") break else: print("成绩必须在 0 到 100 之间,请重新输入。") except ValueError: print("输入无效,成绩必须是数字,请重新输入。")修改的关键点在course not in stu["scores"]这行。因为字典的键是科目名,如果用户手误输成了“语文 ”(带了空格),就会导致修改失败,所以基础版我已经用 strip() 清理过输入。更稳的是把科目选择做成数字选项,比如 1 代表语文、2 代表数学、3 代表英语,再映射到科目名,这样比直接让用户输入中文更不容易出错。但从教学角度,直接输入科目名更能练到字典按键取值。
3.4 删除学生信息:小心删除失败没有提示
删除功能的逻辑是:先按学号找到要删除的学生,找到之后从列表里移除。这里我优先使用学号作为删除依据,因为学号理论上唯一,姓名则可能重名,拿姓名做删除会误删。
def delete_student(): student_id = input("请输入要删除的学生学号:").strip() for i, stu in enumerate(students): if stu["student_id"] == student_id: print(f"即将删除:{stu['name']}(学号:{stu['student_id']})") confirm = input("确认删除?输入 y 确认:") if confirm.lower() == "y": del students[i] print("删除成功!") else: print("已取消删除。") return print("没有找到该学号对应的学生。")这个函数用了 enumerate 拿到当前循环的下标 i,然后del students[i]删除对应位置的元素。用 del 而不是 remove,是因为 remove 需要传入元素对象,而我们手头只有下标,必须把下标算出来。另一种等价的写法是students.remove(stu),但 for 循环里直接 remove 正在遍历的列表,可能引起元素跳过问题,这也是 Python 新手常踩的坑。用下标删除更稳,遍历结束立即 return,也不存在并发修改列表的问题。
有一点要强调:删除是破坏性操作,我在真正执行 del 之前加了一次确认提示。这个习惯在任何写数据的程序里都值得养成,尤其等以后程序升级成可视化界面,用户误操作的成本会更高,所以在命令行版本里就练成“破坏性操作先确认”的好习惯,是很有价值的。
3.5 统计与排序:入门阶段最实用的内置函数
统计功能是这个系统里“数据价值”开始体现的地方。基本需求是:每名学生的各科平均分、全班单科平均分、每名学生的最高分、最低分,以及各科及格率。
def show_statistics(): if not students: print("当前没有任何学生数据,请先录入。") return print("\n===== 班级成绩统计 =====") student_total = 0 for stu in students: scores_list = list(stu["scores"].values()) total = sum(scores_list) avg = total / len(scores_list) max_score = max(scores_list) min_score = min(scores_list) print(f"{stu['name']}:总分 {total:.1f},平均分 {avg:.1f},最高分 {max_score},最低分 {min_score}") student_total += total class_avg = student_total / len(students) print(f"\n全班总平均分:{class_avg:.1f}") for course in ["语文", "数学", "英语"]: course_scores = [stu["scores"][course] for stu in students] course_avg = sum(course_scores) / len(course_scores) pass_count = sum(1 for score in course_scores if score >= 60) pass_rate = pass_count / len(course_scores) * 100 print(f"{course}平均分:{course_avg:.1f},及格率:{pass_rate:.1f}%")统计部分用了不少 Python 内置函数,它们是入门阶段的重点。sum 对列表求和,len 计算长度,max 和 min 找最大最小值。这些函数配合列表推导式可以写得很精简,但为了让基础薄弱的学习者能看懂,我在及格率统计时保留了一个sum(1 for score in course_scores if score >= 60)的写法,它的含义是“遍历所有成绩,统计大于等于 60 分的个数”,等效于一个计数器 for 循环。这种“列表推导式 + 条件”的写法非常高效,也顺便练到了条件表达式。
排序功能的实现也不难,但有个容易出错的细节:
def total_score(stu): return sum(stu["scores"].values()) def sort_students_by_total(): if not students: print("当前没有任何学生数据,请先录入。") return ranked = sorted(students, key=total_score, reverse=True) print("\n===== 按总分从高到低排序 =====") for rank, stu in enumerate(ranked, start=1): total = total_score(stu) print(f"第{rank}名:{stu['name']}(学号:{stu['student_id']}),总分:{total:.1f}")sorted 不会修改原始列表,而是返回一个新的排好序的列表,所以这里的ranked = sorted(...)必须接收返回值。key 参数指定排序依据,我传的是 total_score 函数,sorted 会对列表里的每个元素调用一次这个函数,用返回的总分进行比较。reverse=True 表示降序排列。
这一段的重点是讲清楚“函数也是对象”这个概念:total_score 这个名字本身就可以传给 key 参数,不需要加括号。如果写成key=total_score(),程序会在排序前就执行一次,然后返回一个数字,这个数字没法对每个学生调用,必然报错。这类细节在实际开发中经常困扰初学者,我在教案里会专门强调。
3.6 完整代码汇总
把前面的函数全部拼在一起,整个项目的完整代码就是这样。建议初学者不要复制粘贴,尽量一个函数一个函数地手动敲,敲的过程中自然会记住语法结构。
students = [ {"name": "张三", "student_id": "001", "scores": {"语文": 88, "数学": 90, "英语": 92}}, {"name": "李四", "student_id": "002", "scores": {"语文": 77, "数学": 85, "英语": 81}}, ] def add_student(): name = input("请输入学生姓名:").strip() if not name: print("姓名不能为空!") return student_id = input("请输入学号:").strip() for stu in students: if stu["student_id"] == student_id: print("该学号已存在,不能重复录入!") return scores = {} for course in ["语文", "数学", "英语"]: while True: try: score = float(input(f"请输入{course}成绩:")) if 0 <= score <= 100: scores[course] = score break else: print("成绩必须在 0 到 100 之间,请重新输入。") except ValueError: print("输入无效,成绩必须是数字,请重新输入。") students.append({"name": name, "student_id": student_id, "scores": scores}) print(f"学生 {name}(学号 {student_id})的成绩录入成功!") def show_all_students(): if not students: print("当前没有任何学生数据,请先录入。") return for stu in students: print(f"姓名:{stu['name']},学号:{stu['student_id']}") for course, score in stu["scores"].items(): print(f" {course}:{score}") def find_student(keyword): for stu in students: if stu["student_id"] == keyword or stu["name"] == keyword: return stu return None def search_student(): keyword = input("请输入要查询的学号或姓名:").strip() stu = find_student(keyword) if stu: print(f"姓名:{stu['name']},学号:{stu['student_id']}") for course, score in stu["scores"].items(): print(f"{course}:{score}") else: print("没有找到对应的学生。") def modify_score(): keyword = input("请输入要修改的学生学号或姓名:").strip() stu = find_student(keyword) if not stu: print("没有找到对应的学生。") return print("当前成绩:") for course, score in stu["scores"].items(): print(f"{course}:{score}") course = input("请输入要修改的科目名称(语文/数学/英语):").strip() if course not in stu["scores"]: print("科目不存在,无法修改。") return while True: try: new_score = float(input(f"请输入新的{course}成绩:")) if 0 <= new_score <= 100: stu["scores"][course] = new_score print("修改成功!") break else: print("成绩必须在 0 到 100 之间,请重新输入。") except ValueError: print("输入无效,成绩必须是数字,请重新输入。") def delete_student(): student_id = input("请输入要删除的学生学号:").strip() for i, stu in enumerate(students): if stu["student_id"] == student_id: print(f"即将删除:{stu['name']}(学号:{stu['student_id']})") confirm = input("确认删除?输入 y 确认:") if confirm.lower() == "y": del students[i] print("删除成功!") else: print("已取消删除。") return print("没有找到该学号对应的学生。") def show_statistics(): if not students: print("当前没有任何学生数据,请先录入。") return print("\n===== 班级成绩统计 =====") student_total = 0 for stu in students: scores_list = list(stu["scores"].values()) total = sum(scores_list) avg = total / len(scores_list) max_score = max(scores_list) min_score = min(scores_list) print(f"{stu['name']}:总分 {total:.1f},平均分 {avg:.1f},最高分 {max_score},最低分 {min_score}") student_total += total class_avg = student_total / len(students) print(f"\n全班总平均分:{class_avg:.1f}") for course in ["语文", "数学", "英语"]: course_scores = [stu["scores"][course] for stu in students] course_avg = sum(course_scores) / len(course_scores) pass_count = sum(1 for score in course_scores if score >= 60) pass_rate = pass_count / len(course_scores) * 100 print(f"{course}平均分:{course_avg:.1f},及格率:{pass_rate:.1f}%") def total_score(stu): return sum(stu["scores"].values()) def sort_students_by_total(): if not students: print("当前没有任何学生数据,请先录入。") return ranked = sorted(students, key=total_score, reverse=True) print("\n===== 按总分从高到低排序 =====") for rank, stu in enumerate(ranked, start=1): total = total_score(stu) print(f"第{rank}名:{stu['name']}(学号:{stu['student_id']}),总分:{total:.1f}") def main(): while True: print("\n===== 学生成绩管理系统(基础版) =====") print("1. 录入学生成绩") print("2. 显示全部学生") print("3. 按学号或姓名查询学生") print("4. 修改学生成绩") print("5. 删除学生") print("6. 统计成绩") print("7. 按总分排序显示") print("0. 退出系统") choice = input("请输入操作编号:") if choice == "1": add_student() elif choice == "2": show_all_students() elif choice == "3": search_student() elif choice == "4": modify_score() elif choice == "5": delete_student() elif choice == "6": show_statistics() elif choice == "7": sort_students_by_total() elif choice == "0": print("感谢使用,再见!") break else: print("无效的选项,请输入 0-7 之间的数字。") if __name__ == "__main__": main()这段代码可以直接保存为 score_manage.py 运行。运行时先在初始数据基础上试试“显示全部学生”,再尝试录入一个新学生,接着做一次统计和排序,最后退出。把流程完整走一遍,基本上就理解了各函数之间的调用关系。
4. 典型报错与排查经验:我在教学和实战里最常遇到的坑
4.1 变量作用域:为什么函数里改了列表却没生效
这是出现频率最高的问题。刚写这个系统时,很多人会这样写:
def add_student(): students = students + [{"name": name, ...}]然后运行录入功能,系统提示“录入成功”,但返回主菜单再显示全部学生,新学生不见了。原因很简单:函数内部的students = ...创建了一个新的局部变量,跟全局的 students 不是同一个对象,函数结束后这个局部变量就没了。
正确的做法是直接操作全局列表的原地方法,比如students.append(...)或del students[i],这样就不需要写global。因为 students 本身是可变对象,函数里调用 append 和 del 修改的是同一个列表对象。但如果写的是“重新赋值”,比如students = sorted(students, key=...),那必须在函数开头声明global students才能让外部的变量重新指向新列表。
我建议基础版里所有修改都原地进行,避免 global 关键字带来的混乱。这也是为什么排序函数用 sorted 返回新列表,但只用于打印,不修改原数据。
4.2 输入校验与异常处理怎么兜底
这个项目里最典型的崩溃场景是:用户输入成绩时手快打了“abc”或者直接回车,float("abc") 立刻抛出 ValueError,程序中断。没有异常处理的话,整个菜单直接白屏退出,体验非常差。
解决思路就是我在 add_student 里示范的 try-except 结构:把可能出错的转换操作放进 try 里,捕获到 ValueError 后打印提示,然后用 while 循环让用户重新输入。这里有个细节:while True 和 break 的位置要放对,赋值成功且校验通过时才能 break,否则回一直循环。
还有就是中文输入法下的全角数字问题,比如用户输入“88”而不是“88”,float 会处理不了。这个问题在真实场景里防不胜防,稳妥的做法是先用.replace("8", "8")这类替换把全角字符转成半角,但基础版先不引入这个复杂度。教学时可以作为课后思考题丢给学生,锻炼他们处理真实输入的能力。
4.3 Windows 控制台中文乱码或编码报错
Windows 的默认控制台编码有可能是 GBK,而 Python 3 的源码默认 UTF-8,当 print 里出现特殊符号或某些中文字符时,可能抛UnicodeEncodeError。这个项目里的输出绝大多数是中文字符,GBK 通常能正常显示,但一旦加入了温度符号、emoji 或者繁体字,就可能踩坑。
解决思路有两个。一是在代码开头加两行:
import sys import io sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')这相当于把标准输出强制改成 UTF-8。二是在 VSCode 的 settings.json 里设置终端编码,或者在系统设置里启用“Beta: 使用 Unicode UTF-8 提供全球语言支持”。我不太推荐初学者直接用第一种方案绕开问题,而是希望他们理解:编码问题本质是“程序输出的字符集”和“终端能显示的字符集”不一致造成的。理解了这一点,遇到类似的报错才能举一反三。
4.4 缩进错误和“意外缩进”
Python 靠缩进区分代码块,混用 Tab 和空格会导致TabError。VSCode 默认会把 Tab 转成空格,问题还小一些,如果用记事本或者某些古老编辑器写代码,就很容易踩坑。解决方法是写代码前先确认编辑器右下角的“空格: 4”或类似标识,全项目统一用 4 个空格缩进。
另一个常见问题是缩进层级不对。比如 while True 下面的 if 分支里的代码,应该在 if 内部还是 if 外部,初学者经常搞混。判断方法很简单:想一想“这行代码是在条件成立时才执行,还是每次循环都会执行”。把这个问题想清楚,缩进就不会错。
下面用表格把常见问题整理一遍,方便大家对照排查:
| 错误信息 | 常见原因 | 处理方式 |
|---|---|---|
| NameError: name 'students' is not defined | 函数内部给 students 重新赋值,没有声明 global | 原地修改列表,或在函数开头加 global students |
| ValueError: could not convert string to float | 输入了非数字内容 | 用 try-except 捕获,让用户重新输入 |
| TabError: inconsistent use of tabs and spaces | 混用 Tab 和空格 | 统一用 4 个空格缩进 |
| UnicodeEncodeError: 'gbk' codec can't encode character | 输出字符超出终端编码范围 | 修改终端编码,或强制设置标准输出为 UTF-8 |
| 函数里改了数据但没有效果 | 函数内重新赋值了外部变量 | 使用 append、del 等方法原地修改,避免重新赋值 |
| 程序打印完一个功能就退出 | 主循环没有正确使用 while True | 确认菜单函数在 while 循环内部,退出条件为 choice == "0" |
5. 从基础版往后怎么走
基础版只把“内存里的数据”管起来,实际使用价值其实有限,因为它没有解决数据持久化问题:程序一退出,所有录入的成绩都没了。后续的系列教案里,我会沿着下面几个方向做扩展,这里先给个预告。
第一个方向是加文件存储。用 Python 自带的 csv 模块,把 students 列表写入 CSV 文件,下次启动时再读回来,大概只需要增加两个函数:save_data() 和 load_data()。原理并不复杂:把每个学生字典的键值对展平成一行 CSV,读取时再反过来恢复成字典。这是“数据持久化”最轻量的实现方式。
第二个方向是面向对象重构。基础版用函数 + 全局列表组织代码,代码量到三四百行以后,维护起来就会渐渐吃力。第二步可以引入 Student 类和 ScoreManageSystem 类,把 add、delete、search 这些操作变成类的方法,全局列表变成类的属性,数据和行为就绑定在一起了。这一步是理解 “类” 和 “对象” 的最佳实践场景。
第三个方向是引入 SQLite 数据库。当数据量变大,CSV 文件读写效率下降,并且并发访问会出现问题,数据库的优势就体现出来了。SQLite 是 Python 内置的轻量数据库,不需要单独安装服务,插入、查询、更新、删除都用 SQL 语句完成,初学数据库也从这里切入非常合适。
我个人的经验是,学编程千万不要只看不练。这一篇里的代码虽然不到两百行,但包含了很多入门阶段极其重要的语法点和编程思想,初学者如果完全靠复制粘贴跑通,收获会打折扣。建议每一段代码都自己敲一遍,敲完再尝试做几个小改动,比如把科目从三门改成五门、把及格线从 60 改成 90、给排序加上“同分按学号升序”的规则。这些改动都是在现有代码上做扩展,正好能检验你是不是真的理解了每个函数在干什么。下一篇教案会在这个基础上加入文件存储版的学生成绩管理系统,到时候我们再聊怎么把“程序关闭后数据不丢”这件事落地。