从零实现学生成绩管理系统:Python命令行增删改查与JSON持久化
最近连续帮几个新手看代码发现大家最容易卡住的地方很统一不是第一道 Hello World而是后面那个标题简洁到只有“第二次作业”的任务。它通常不再只是让你输出一个图案而是要独立做出一个小系统比如命令行版本的学生成绩管理、一个记账小工具或者一份自动汇总的数据表格。今天这篇文章我就用一份典型的“第二次作业——学生成绩管理系统”为例把从读题、拆功能到写代码、测 bug 的完整流程走一遍顺便把我这些年踩过的坑、总结出来的捷径都交代清楚。适合正在写类似任务、或者刚开始接触真实项目逻辑的朋友。1. 先想清楚第二次作业到底在考什么1.1 它和第一次作业的最大区别第一次作业往往是“单点题”比如算个阶乘、打印一个三角形。你只要把教程里的语法用对基本就能过。但第二次作业不一样它一般要求你同时处理“增、删、改、查、存储”这几件事而且这些事之间是有依赖关系的。用一个生活化的例子说第一次作业像做一道番茄炒蛋流程固定翻车点少。第二次作业像做一顿三菜一汤你要同时考虑先煮饭还是先洗菜、哪道菜凉了不好吃、灶台不够用怎么排时间。代码里也一样添加了数据之后查询的代码能不能看到删掉一条记录后文件里的数据有没有跟着变这些都不是某个单独语法能解决的它考验的是你处理“多个模块协作”的能力。所以拿到这种题目先别觉得“这是个学生管理系统太简单了”。你要意识到它其实是一个微缩版的项目背后的考察点是结构设计、用户交互、异常处理和持久化。1.2 透过需求清单看能力点我习惯把题目里的动词全部圈出来因为这些动词就是功能功能后面才对应技术点。比如一份常见的学生成绩管理题目表面上是这几件事添加学生及成绩删除指定学生按姓名或者学号查询修改成绩数据保存到文件下次启动还能读回来把它们翻译成能力点大概就是这样题目需求考核能力消灭的常见病添加学生数据结构的组织能力输入解析能力把变量当数据库不会存数据删除、修改列表遍历、条件匹配、原地更新只删了显示没删到内存里查询查找算法、逻辑分支只会用 if 堆不考虑找不到的情况文件保存序列化和反序列化数据存在内存一关就没了菜单循环主流程设计、状态控制写完一个功能就不知道程序怎么退出带着这个视角去做题你就能明白一个很关键的点第二次作业不是让你背语法而是看你能不能把“需求”翻译成“功能”再把“功能”拆成“函数”。这一步想通后面所有代码都是水到渠成的事。2. 动键盘之前先花半小时做这三件事很多新手拿到题目就开始噼里啪啦写代码写两小时然后陷入“哪里都不对”的泥潭。我自己的习惯是相反先花半小时把地基打清楚。这部分做扎实了后面的开发时间至少省一半。2.1 把题目翻译成功能清单读题至少读三遍。第一遍看大方向第二遍圈动词第三遍把每个动词变成可执行的函数名。比如“支持添加、删除、按姓名查询、按学号修改、保存到文件”这句话翻译之后应该变成add_student()delete_student()search_student()modify_student()save_data()load_data()这套函数清单就是你代码的骨架。很多人写代码乱往往是因为没有提前列出函数清单写到后面自己都忘了系统里有哪些入口。我在实际操作中还会顺手标注每个函数大概需要什么参数、返回什么。比如modify_student()需要先接收一个学号作为查找条件再接收新的成绩作为更新值。这个标注不一定要很正规写在草稿纸上都行但是一定要有。因为它能逼你把流程走一遍避免写第一个函数的时候才去想第二个函数该长什么样。2.2 画出最简单的数据流不需要画复杂的流程图只要用几行字描述清楚数据是怎么流动的就行。对于学生成绩管理系统数据流大概是程序启动先尝试从students.json文件读取历史数据。用户从终端输入命令比如“1”代表添加“2”代表删除。程序根据命令调用对应的函数修改内存里的students列表。用户选择退出或者在我们主动触发的保存流程里把students列表写回students.json。这个看起来非常简单的数据流其实解决了正确设计程序的精髓。很多人会犯的一个错误是每次添加完学生都去写一次文件或者退出的时候忘了保存结果数据还停留在上一次启动时的旧状态。如果你能先让“读取、修改内存、写回文件”这条链路在脑子里清楚这些问题都不会发生。2.3 先用伪代码搭骨架伪代码可能是整个过程中最廉价、收益最高的工具。你不用纠结具体语法只要把流程写出来显示菜单 获取用户输入 choice 如果 choice 1: 调用添加函数 如果 choice 2: 调用删除函数 如果 choice 3: 调用查询函数 如果 choice 0: 保存数据 退出循环 否则: 提示输入无效这份伪代码其实已经是一个可以运行的框架雏形。你甚至可以先用pass占位把每个函数都定义出来让主循环先跑通再去补齐函数体。这个做法特别适合新手因为“让程序从一开始就能运行”会给后续调试省下大量心力和时间。3. 核心实现一份可以直接抄的学生成绩管理系统下面我给出一套非常老派但是足够稳的示例代码。它没有用花哨的框架用的就是 Python 标准库你在任何一台装了 Python 的机器上都能跑起来。3.1 数据结构选型别一上来就写类我看到不少同学喜欢在最开始就定义一个Student类然后把所有方法都往里面塞。这个想法本身没问题但“第二次作业”的复杂度通常撑不起类带来的收益。我建议这一阶段用更简单的list[dict]。每个学生就是一个字典字段大概是students [ {sid: 2024001, name: A同学, score: 88} ]为什么用字典而不是类第一字典天然映射 JSON保存和读取都不用做额外转换。第二Python 新手对student.name这种访问方式还没形成肌肉记忆用student[name]更直观出问题的概率也更低。第三考试或者面试时主考官更想看的是你把数据组织得是否清晰而不是堆了多少面向对象语法。等到你开始处理几十个字段、需要方法互相协作的时候再重构为类一点不迟。这段代码还隐含了一个很重要的习惯字段名用固定的字符串常量。比如你写sid写顺手了很容易在某个地方写成s_id然后程序莫名其妙找不到键。我自己早期就吃过很多这种亏后来会在心里默默提醒自己每个键都要保持一致任何分隔符变量名都可能是隐患。3.2 文件持久化用 json 而不是自己造格式数据要存下来常见的选择是 CSV、纯文本、JSON。对“第二次作业”来说JSON 是综合体验最好的方案因为 Python 内置的json模块可以直接把list和dict转成文本也能直接读回内存。保存和读取的代码可以这样写import json def save_data(): with open(students.json, w, encodingutf-8) as f: json.dump(students, f, ensure_asciiFalse, indent2) def load_data(): global students try: with open(students.json, r, encodingutf-8) as f: students json.load(f) except FileNotFoundError: students []这里有三个细节非常值得注意。第一global students很有必要因为你在函数里要给外面的列表重新赋值不是单纯修改列表内容。如果你只是在load_data()里写students json.load(f)那这个新列表是函数的局部变量函数一结束就丢了程序里还是空的。第二ensure_asciiFalse一定要加不然字典里的中文会被转成\uXXXX形式看起来像乱码。第三indent2是纯为了人眼调试舒服你说它是个人偏好也行但实际排错时真的能救命。另外我习惯在load_data()里用try...except捕获文件不存在的情况。这么做不是因为懒而是因为“第一次运行根本没有文件”这件事是必然发生的。与其让程序崩溃不如默认给一个空列表。3.3 菜单主循环与增删改查现在搭建主循环。def main(): load_data() while True: print(\n1. 添加学生) print(2. 删除学生) print(3. 查询学生) print(4. 修改成绩) print(0. 退出并保存) choice input(请输入序号) if choice 1: add_student() elif choice 2: delete_student() elif choice 3: search_student() elif choice 4: modify_student() elif choice 0: save_data() print(已保存再见) break else: print(输入无效请重新选择) if __name__ __main__: main()这个主循环里有一个非常容易被忽略的点退出分支里先调用save_data()再break。顺序绝对不能反。如果先break程序就退出了保存代码根本不会执行数据自然丢失。增删改查函数可以写得很朴素比如def add_student(): sid input(请输入学号) for s in students: if s[sid] sid: print(该学号已存在) return name input(请输入姓名) score input(请输入成绩) students.append({sid: sid, name: name, score: int(score)}) print(添加成功)add_student()里为什么要先遍历一遍检查学号因为学号作为唯一标识如果允许重复删除和修改时就会混乱。这个约束你不写代码也能想出来属于需求分析的一部分而不只是语法问题。另外int(score)这里如果没有异常处理遇到非数字输入会直接崩溃。你可以暂时用最笨的方法——提示用户重新输入或者更稳一点用try...except捕获后给出友好提示。删除函数我用索引方式处理def delete_student(): sid input(请输入要删除的学号) for i, s in enumerate(students): if s[sid] sid: students.pop(i) print(删除成功) return print(未找到该学号)这里用enumerate拿到下标i然后pop(i)删除。为什么不直接students.remove(s)因为当列表里有多个学号相同的记录时remove只删除第一个而且你在查找的时候已经找到了一条删完就return所以用索引是更明确的方式。更重要的是在循环里一边遍历一边remove会让你跳过后面的元素这类 bug 对新手来说特别隐蔽。查询和修改的实现类似。修改特别容易踩坑的地方是你找到了目标学生也改了成绩但是没有把它同步到students这个列表里。比如有人会写s[score] new_score结果发现外面的变量s是从列表里取出来的引用其实修改已经生效但是他自己打印的时候又去读了一次旧文件误以为没改。所以要记住如果你在内存里操作的是同一个列表对象那么s[score] new_score就是有效的如果你重新从文件读了一遍那又是另外一码事了。3.4 边界情况与交互体验“第二次作业”想拿高分光跑通主流程是不够的。我记得有个同学交了作业功能都能跑但是一输入英文字母成绩就崩。这类问题在作业讲评时经常被点名也很容易丢分因为说明你完全没有考虑用户的输入是不可控的。我给这段代码加一个最基础的保护def read_score(): while True: try: score input(请输入成绩) return int(score) except ValueError: print(成绩必须是数字请重新输入)然后add_student()里直接调用read_score()而不是int(input(...))。看起来多写了几行但用户体验完全不一样。命令行程序虽然简陋但一个好的交互反馈会让你在老师或者面试官眼里比大多数人专业。另一个边界是如果文件里已经存在 100 条数据查询一个不存在的学号程序要能给出“未找到”而不是直接抛异常或者什么都不做。很多新手只会处理“找到”的分支忘记写“找不到”的分支结果点击查询之后程序默默结束没有任何提示用户根本不知道发生了啥。4. 提交前的测试与避坑清单4.1 高频翻车点速查表我把这些年批作业和帮人调试时遇到最多的几个问题整理成一张表。强烈建议你写完代码之后对着这张表检查一遍问题现象最常见原因解决思路提示FileNotFoundError第一次运行没有旧数据文件用try...except捕获后初始化为空列表打开 JSON 文件看到\uXXXX保存时没有设置ensure_asciiFalse保存时加上该参数中文在终端里显示乱码文件保存编码和终端编码不一致统一使用encodingutf-8删掉一条记录后列表没变小循环里用了错误的删除方式或者删除后没有return用索引pop删完立即退出函数数据重启后消失退出时没有调用保存函数或者保存函数根本没执行把保存放在break之前JSON 文件被手改坏后读不了没有做异常处理捕获json.JSONDecodeError并提示用户数据文件损坏这张表看起来都是一行字但每条背后都对应着至少两位数的调试时间。我不是想吓唬你而是想说这些坑实在太经典了经典到几乎所有人都会踩一遍。你能提前避开就已经超过大多数人。4.2 我最常用的三个调试技巧第一个技巧把数据状态直接打印出来。代码卡住的时候不要猜直接打印print(students)或者print(len(students))。很多时候你以为变量没变其实只是打印的位置不对。加上一行日志立刻就能看清楚问题出在添加、删除还是保存环节。第二个技巧在保存和读取之间做“隔墙测试”。你可以在一个脚本里先导入函数添加两条假数据调用save_data()然后把进程重启看看能不能读到那两条数据。这个过程能直接验证持久化是否正确而不用反复手动操作菜单。这个测试成本很低收益却非常高。第三个技巧用系统自带的文本编辑器打开生成的students.json看看里面的结构。如果你预期是students是一个列表里面的元素是字典那文件里就应该显示类似[{sid: 2024001, ...}]。如果结构不符合预期问题往往出现在json.dump的参数上而不是业务函数里。大多数人只盯着终端输出却忽略了中间产物和预期不一致这个最简单的破绽。4.3 交作业前的自测清单最后我把每次提交作业前都会跑的自测流程写成一个清单。这个清单也适用于其它类似的小项目程序能不能从“完全不存在文件”的状态正常启动。添加两条相同学号的数据系统必须拒绝第二条。删除一条数据查询时确认已消失文件里也没有这条数据。修改成绩后先查内存数据再保存退出重启程序确认数据还在。输入非数字成绩时程序应当提示错误而不是直接崩溃。输入一个不在菜单里的符号比如8或字母程序应当回到菜单而不是报错。在退出前故意不保存重启后数据应该是旧数据这虽然看起来是 bug但你至少要知道它的行为。我的习惯是每完成一项测试就在草稿纸上打个勾。别嫌麻烦这种项目的功能点其实不多跑完所有场景可能只需要十分钟但它能帮你在交作业前拦截掉九成低级问题。最后再分享一个经验写“第二次作业”最舒服的方式不是一鼓作气把代码全部写完而是先把骨架搭好让主循环能转起来再一个函数一个函数地填内容。每填完一个函数立刻跑一次测试。这样做的好处是你永远知道当前报错来自哪个具体模块而不是让一堆错误混在一起互相干扰。这个小习惯我到现在还在用而且它确实让我少加了很多次班。