Python学生成绩管理系统教程:从零搭建命令行CRUD实战

Python学生成绩管理系统教程:从零搭建命令行CRUD实战 学生成绩管理系统这个题目第一次带 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 文件按 CtrlShiftP 调出命令面板输入 “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, keytotal_score, reverseTrue) print(\n 按总分从高到低排序 ) for rank, stu in enumerate(ranked, start1): total total_score(stu) print(f第{rank}名{stu[name]}学号{stu[student_id]}总分{total:.1f})sorted 不会修改原始列表而是返回一个新的排好序的列表所以这里的ranked sorted(...)必须接收返回值。key 参数指定排序依据我传的是 total_score 函数sorted 会对列表里的每个元素调用一次这个函数用返回的总分进行比较。reverseTrue 表示降序排列。这一段的重点是讲清楚“函数也是对象”这个概念total_score 这个名字本身就可以传给 key 参数不需要加括号。如果写成keytotal_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, keytotal_score, reverseTrue) print(\n 按总分从高到低排序 ) for rank, stu in enumerate(ranked, start1): 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”float 会处理不了。这个问题在真实场景里防不胜防稳妥的做法是先用.replace(, 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, encodingutf-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 studentsValueError: could not convert string to float输入了非数字内容用 try-except 捕获让用户重新输入TabError: inconsistent use of tabs and spaces混用 Tab 和空格统一用 4 个空格缩进UnicodeEncodeError: gbk codec cant encode character输出字符超出终端编码范围修改终端编码或强制设置标准输出为 UTF-8函数里改了数据但没有效果函数内重新赋值了外部变量使用 append、del 等方法原地修改避免重新赋值程序打印完一个功能就退出主循环没有正确使用 while True确认菜单函数在 while 循环内部退出条件为 choice 05. 从基础版往后怎么走基础版只把“内存里的数据”管起来实际使用价值其实有限因为它没有解决数据持久化问题程序一退出所有录入的成绩都没了。后续的系列教案里我会沿着下面几个方向做扩展这里先给个预告。第一个方向是加文件存储。用 Python 自带的 csv 模块把 students 列表写入 CSV 文件下次启动时再读回来大概只需要增加两个函数save_data() 和 load_data()。原理并不复杂把每个学生字典的键值对展平成一行 CSV读取时再反过来恢复成字典。这是“数据持久化”最轻量的实现方式。第二个方向是面向对象重构。基础版用函数 全局列表组织代码代码量到三四百行以后维护起来就会渐渐吃力。第二步可以引入 Student 类和 ScoreManageSystem 类把 add、delete、search 这些操作变成类的方法全局列表变成类的属性数据和行为就绑定在一起了。这一步是理解 “类” 和 “对象” 的最佳实践场景。第三个方向是引入 SQLite 数据库。当数据量变大CSV 文件读写效率下降并且并发访问会出现问题数据库的优势就体现出来了。SQLite 是 Python 内置的轻量数据库不需要单独安装服务插入、查询、更新、删除都用 SQL 语句完成初学数据库也从这里切入非常合适。我个人的经验是学编程千万不要只看不练。这一篇里的代码虽然不到两百行但包含了很多入门阶段极其重要的语法点和编程思想初学者如果完全靠复制粘贴跑通收获会打折扣。建议每一段代码都自己敲一遍敲完再尝试做几个小改动比如把科目从三门改成五门、把及格线从 60 改成 90、给排序加上“同分按学号升序”的规则。这些改动都是在现有代码上做扩展正好能检验你是不是真的理解了每个函数在干什么。下一篇教案会在这个基础上加入文件存储版的学生成绩管理系统到时候我们再聊怎么把“程序关闭后数据不丢”这件事落地。