大二那周的Python课结束之后我拿到了一份“python第二次作业”题目不算复杂让程序能够录入学生成绩、查询、修改、删除、统计和保存文件。真正卡住我的不是语法而是怎么把之前零零散散学过的列表、字典、函数、文件操作串成一个能跑通的整体。我以为自己掌握了基础结果一动手才发现从“会写代码片段”到“能完成一个完整小项目”之间还有一道看不见的坎。这篇博文就把我完成这次作业的完整过程、踩过的坑、改过的版本和最后的代码结构从头到尾写一遍适合正在写类似基础作业、或者刚学完Python基础语法想练手的人参考。1. 拿到作业后的第一件事先拆需求别急着敲代码1.1 第二次作业到底在考什么第一次作业基本上就是print、变量、if判断、for循环这些“零件”的练习代码量小每个题目都是孤立的。第二次作业最大的区别是它要求把这些零件组合起来做一个有菜单、有数据存储、有重复操作的完整程序。换句话说它考的不是“你会不会写for循环”而是“你敢不敢把一个复杂问题拆成几个简单问题”。我见过不少同学包括当时的我拿到题目第一反应就是打开编辑器开始写代码写到二三十行之后就开始乱变量名混乱、逻辑嵌套越来越深最后删掉重来。这个问题的根源不是手笨而是没有先做需求拆分。一个完整的学生成绩管理程序听起来是个“系统”拆开之后其实就是几个动作录入、展示、查询、修改、删除、统计、保存、读取、退出。每个动作对应一个独立的小任务再把小任务一层层组合起来整个程序就清晰了。1.2 我拆需求的方式把题目里的动词圈出来我习惯用一个很笨但很有效的方法把作业文字里出现的动词全部圈出来每个动词就是一个功能点。比如“录入学生成绩”“查询某个学生的信息”“修改成绩”“删除记录”“计算平均分”“保存到文件”圈完之后再补充隐含的功能“显示所有学生”“退出程序”“读取文件”。这样就有了一张功能清单。录入学生信息学号、姓名、成绩显示全部学生列表按学号查询单个学生按学号修改学生信息按学号删除学生统计最高分、最低分、平均分按成绩排序保存到文件从文件读取启动时自动加载有了这个清单我再看程序结构就非常清楚主菜单负责“跳转”每个功能是一个独立函数数据用同一个结构在函数之间传递。这张清单我大概写了半小时但后面写代码省下来的时间远不止半小时。这也是我这次作业最大的一个心得写代码之前先写需求清单效率反而最高。2. 数据怎么存列表嵌套字典是我最后的选择2.1 一开始我用了三个平行列表然后被下标搞疯了我的第一版数据结构很天真用三个列表分别存学号、姓名、成绩。ids [001, 002] names [张三, 李四] scores [88, 92]这样写起来确实直观第0个位置的id、name、score拼在一起就是一个学生的记录。问题出在删除和修改上如果删了第1个学生另外两个列表的下标要对齐必须同步删如果修改了某个成绩也要记住是哪个下标。更麻烦的是查询的时候经常要通过学号找到下标然后拿着下标去另外两个列表里取数据。三个列表还好万一下次再加一门课的成绩变成四个列表、五个列表这个方案直接崩盘。后来我意识到列表能存的不只是单一的值还能存字典。一条学生的记录本质上是一个整体学号、姓名、成绩是它的三个字段。用三个平行列表等于把一条记录拆成三块散装存放用列表套字典则是一条记录作为一个字典放在一个元素里信息内聚操作简单得多。2.2 列表嵌套字典一条记录是一个整体第二版的数据结构长这样students [ {id: 001, name: 张三, score: 88}, {id: 002, name: 李四, score: 92}, ]访问第二个学生的成绩就是students[1][score]删除学生就是用pop()或者remove()删掉整个字典修改就是先找到下标再改students[i][score] 95。所有操作都围绕“字典”这个整体展开不用再考虑下标对齐问题。这看起来只是换了一个存储方式但实际写代码的感觉完全不一样。原来在函数里要传三个列表参数现在只需要传一个students原来打印一条记录要写三行现在循环遍历字典一次就好。整体代码量大概减少了三分之一而我付出的成本仅仅是多理解一个“字典可以塞进列表里”的概念。2.3 为什么没直接用字典以学号为key有人可能会问直接用students {001: {name: 张三, score: 88}}不是更方便吗按学号查询直接students[001]连遍历都不用。确实用学号做key有两个明显好处查询快、学号天然不能重复。但我也对比了两种方案的缺点。用学号做key要显示所有学生时需要students.values()再排序修改学号这个操作就非常麻烦因为key不能直接改得先建新key、复制值、删旧key。更重要的是作业里通常要求“按成绩排序输出”字典按key存取很方便但按value排序后想要保留下标关系反而绕了一圈。综合下来列表嵌套字典虽然查询时不如字典key那么直接但对于“增删改查排序统计”这种功能组合是更平衡的选择。这也是我建议初学者优先掌握的结构数据规模是几十条几百条时性能差异根本无所谓代码好写、逻辑清楚才是第一位的。3. 五个功能函数的实现顺序与核心逻辑3.1 先写死菜单循环再用一个字典映射函数我先把主菜单搭出来让程序有一个可以一直选择操作的框架然后再把每个功能函数填进去。这个过程就像先搭好房子的框架再装修各个房间心里会踏实很多。菜单部分的核心是while True循环加if/elif/else分支但因为功能函数比较多我最后改成了字典映射的方式def main(): while True: show_menu() choice input(请输入你的选择) if choice 0: save_data(students) print(已保存并退出程序。) break elif choice 1: add_student(students) elif choice 2: show_all(students) # ...用if/elif写这个菜单没有技术问题但分支多了以后菜单代码会变得很长而且每个分支都是一行调用看着有些重复。我也尝试用一个字典把选项和函数对应起来当时觉得那样更“高级”但调试过程中的报错信息不够直观最后还是改回了if/elif。对于第一次做项目的人来说简单、能看到每一步调用逻辑的写法有时候比“优雅”更重要。3.2 录入函数不合法就一直问到合法为止录入学生信息是最基础的功能也是后面所有功能的数据来源。我遇到的第一个问题是如何保证输入的数据是合理的学号能不能为空成绩能不能是负数如果用户输了一个abc这样的成绩程序不应该崩溃。解决方案是给录入函数加上一层输入校验用while循环套着try/except一旦输入不合法就重新提示直到合法才跳出循环。def input_score(prompt请输入成绩): while True: val input(prompt) try: score float(val) if 0 score 100: return score else: print(成绩需要在0到100之间。) except ValueError: print(输入无效请输入一个数字。)这个函数把“校验逻辑”封装起来了后面修改成绩时也能复用不用重复写一遍try/except。我发现好的函数设计不是一次到位的而是在写第二个相似功能时发现可以抽出公共部分。没有这个意识的时候我会把同样的校验代码复制三份改一个地方还要记得去改另外两处非常容易漏。3.3 查询、修改、删除本质都是“先找到这个学号”三个操作都依赖同一个前置动作根据学号找到对应的下标。我一开始在每个函数里都写一遍for循环找学号后来觉得不对这样同样的代码出现了三次不如抽成一个辅助函数。def find_student_index(students, stu_id): for i, stu in enumerate(students): if stu[id] stu_id: return i return -1返回-1表示没找到。这样查询函数就变成拿到下标判断是否等于-1然后打印信息。修改函数变成拿到下标先确认值得改再覆盖字段。删除函数变成拿到下标用students.pop(i)移除整条记录。三个函数都短了很多而且因为找学号的逻辑只写在一处一旦要改成“忽略学号空格”或者“大小写不敏感”只改这个辅助函数就够了。如果在第二次作业里你要实现的功能也有“多个操作共享同一个前置动作”建议你第一时间把公共动作抽出来。这个习惯比任何高级语法都更重要。3.4 统计和排序空列表是最大的隐形炸弹统计功能需要算出最高分、最低分、平均分还要按成绩从高到低输出学生列表。逻辑本身不复杂def show_statistics(students): if not students: print(当前没有学生数据无法统计。) return scores [stu[score] for stu in students] print(f最高分: {max(scores)}) print(f最低分: {min(scores)}) print(f平均分: {sum(scores) / len(scores):.2f})这几行代码看起来很简单但里面藏着一个非常容易踩的坑如果students是空列表max(scores)会直接抛ValueError。我第一次跑程序的时候刚启动还没有录入数据就去菜单里选“统计”程序当场崩溃。后来我养成了一个习惯任何函数都要考虑数据为空的情况。统计前先判断文件是否存在、查询前先判断用户是否输入了空字符串、修改前先判断学生是否存在。把边界条件全部想一遍程序的健壮性会有很大提升。排序输出用的是sorted()配合key参数for stu in sorted(students, keylambda s: s[score], reverseTrue): print(f{stu[id]} {stu[name]} {stu[score]})如果想让分数相同的学生再按学号排只需要把key改成(s[score], s[id])然后注意排序顺序。这里我一开始没有注意reverseTrue会同时反转两个排序条件后来自己设计的小实验帮我搞清楚了key是一个元组时排序逻辑会先比较第一个元素再比较第二个元素reverseTrue会整体反转。搞清楚这个之后我才敢放心调整排序规则。3.5 文件存储我最后选了JSON而不是CSV保存文件的常规选项有JSON和CSV两种。CSV可以用Excel打开看起来很友好JSON则是Python原生支持的格式json.dump()和json.load()一行代码就能读写复杂结构。我的选择是JSON原因有两条。一是json.dump()保存字典列表时结构不会丢读回来以后数据类型保持不变二是它不需要自己处理逗号、引号、换行符这些细节代码少很多。CSV的坑在于如果某个字段里本身带了逗号或者引号就需要额外处理初学者很容易写出有问题的保存逻辑。import json def save_data(students, filenamestudents.json): with open(filename, w, encodingutf-8) as f: json.dump(students, f, ensure_asciiFalse, indent2) def load_data(filenamestudents.json): try: with open(filename, r, encodingutf-8) as f: return json.load(f) except FileNotFoundError: return []这个实现里有几个细节值得说明。第一encodingutf-8一定要写不写的话在Windows系统上默认编码可能是GBK保存中文姓名时读回来会乱码。第二ensure_asciiFalse是为了让中文直接以明文形式存在文件里否则JSON里会是一堆\u5f20\u4e09这样的转义字符人没法直接看。第三加载文件时用try/except FileNotFoundError处理第一次运行没有数据文件的情况返回一个空列表程序就能正常启动了。4. 练习册上找不到的坑我从调试里学到的东西4.1 函数返回值与列表修改的“作用域错觉”这是我这次作业里花时间最长的一个BUG。我最初把增加学生逻辑写成一个函数但在函数内部重新给students赋值了def add_student(students): ... students students [new_student] # 问题出在这里在函数外部调用add_student(students)之后我再用show_all(students)打印发现新学生并没有出现在列表里。原因是students students [new_student]这条语句对局部变量students重新绑定了一个新列表函数外部原来的列表并没有被修改。改成students.append(new_student)之后问题就消失了因为append是直接操作原列表对象不改变students的指向。我后来总结出一个方便记忆的结论在函数里如果你使用赋值语句修改参数变量本身那么通常是重新绑定局部变量如果你使用方法如append、pop修改对象的内容那么就能影响外部看到的数据。这也是为什么大量初学者写菜单程序时增删改查没有效果不是逻辑错了而是根本没修改到正确的数据对象。4.2 空列表调用max()直接崩溃我被Traceback教育了前面提到过统计空列表会报错但真正触发这个错误的场景很典型我写完保存读取功能后为了测试先把students.json文件手动删掉了然后启动程序直接按“3”查看统计信息。屏幕上出现一长串Traceback最后一行是ValueError: max() arg is an empty sequence。一开始我甚至没反应过来是空列表导致的因为我觉得“程序刚启动没有数据统计为空”是很正常的情况怎么就直接崩溃了呢事后我检查才明白max()和min()不接收空序列作为参数这是Python的明确限制。解决方案有两个要么在调用前判断if not scores要么给max()传入默认值max(scores, default0)。我在统计函数开头加了“空数据提前返回”的判断后面所有依赖列表数据的函数也都加了一层保护。这个习惯让我在后续其他作业里少踩了很多雷。4.3 保存中文乱码编码问题让我学会了看文件头程序写完之后我把生成的students.json文件用文本编辑器打开发现里面正常显示中文自己还挺高兴。结果把students.json发给另一个同学让他帮忙测试读取他那边的程序读出来全是乱码。折腾了半天才发现他用的操作系统Python语言环境默认编码和我这台机器不一样——我在写入时没有显式指定编码格式读取时也没指定。后来我统一改成encodingutf-8写文件、encodingutf-8读文件问题就再没有出现过。在Python里处理一切包含中文的文件读写我现在的铁律就是永远显式指定编码绝不依赖系统默认值。不只是JSONCSV、TXT都一样。5. 用10条测试数据把程序跑了一遍5.1 手工测试路径设计代码写完之后我没有直接交上去而是设计了一套测试路径把每个功能都走了一遍。这个习惯养成之后后来的作业基本不会出现“老师点一下菜单就崩溃”的尴尬。测试顺序是这样的启动程序确认菜单显示正常。选择录入依次录入10条不同成绩的学生数据包括一个成绩为0、一个成绩为100的边界值。选择显示全部确认10条记录都在。输入一个存在的学号查询再输入一个不存在的学号查询确认两种情况都正常。用成绩重复的学号做修改验证修改后显示是否正确。删除一条记录再显示全部确认少了一条。查看统计信息手动计算平均分核对程序输出。选择排序输出确认是从高到低。退出程序查看生成的json文件内容。重新启动程序确认能自动加载之前保存的数据。这套路径覆盖了正常操作和边界操作基本上功能性的问题都能被发现。5.2 运行结果样例与分析下面是我当时测试时截取的一段输出数据已简化 学生成绩管理菜单 1. 录入学生 2. 显示全部 3. 查询学号 4. 修改成绩 5. 删除学生 6. 统计信息 7. 按成绩排序 8. 保存到文件 0. 退出 请输入菜单选项6 当前共有学生 10 人 最高分: 100.0 最低分: 0.0 平均分: 82.30我手动算了算平均分发现程序输出的结果和计算器一致说明统计逻辑没写错。排序功能输出时能看到成绩从100到0依次排列也没有问题。最让我有信心的一步是保存后重启程序上一次录入的学生数据全部还在说明文件读写闭环跑通了。6. 交作业前我又做了三处收尾优化6.1 把重复的“输入一个学号”抽成函数写查询、修改、删除三个功能的时候我都写了一遍“请输入学号”的代码内容几乎一样只是后面的提示语不同。于是我抽了一个input_student_id()函数专门处理学号输入可以在函数内部顺手去掉首尾空格还可以检查是否为空。抽完以后三个功能函数的代码又缩短了一截。一个有用的判断标准同一段代码出现了三次以上就值得抽出来。6.2 给函数加上docstring注释写给自己看以前我写代码不爱写注释觉得代码能跑就行。但这个作业的函数越来越多我发现隔一天再看自己的代码有些函数想不起来是干嘛的。于是我花了一点时间给每个函数加上了docstring简单描述参数和返回值。def find_student_index(students, stu_id): 根据学号查找学生下标。 参数: students: 学生列表元素为字典 stu_id: 要查找的学号 返回: 找到返回对应下标未找到返回 -1 这不算什么高深的技巧但它让代码在读者包括将来的自己面前变得更友好。第二次作业最大的收获之一就是让我意识到注释不是写给老师看的是写给未来接手代码的人很可能就是几个月后的自己看的。6.3 加上if __name__ __main__:入口最后我把整个程序改成了“定义函数 主函数调用”的结构if __name__ __main__: main()这样做的好处是如果别人把这份代码当模块导入main()不会自动运行各种函数可以被复用。对于第二次作业这个规模这个写法可能显得有一点“过度工程”但它帮我建立了“脚本文件也可以有清晰入口”的意识。即使老师不要求我还是推荐加上这一行这是一个好习惯。我在这次作业里最大的体会是第一次作业拼的是“记住知识点”第二次作业拼的是“把知识点串起来”。串起来的过程必然伴随着混乱、报错、推翻重来但只要先把功能拆清楚、数据设计选明白、函数边界想周全后面填代码只是时间问题。如果你也正在写自己的Python第二次作业不妨先别急着敲键盘拿张纸画一画功能清单想清楚数据怎么存再动手。这个顺序比多背十个语法都顶用。