Python成绩统计作业复盘:从平行列表到字典与函数重构

📅 发布时间:2026/10/12 5:52:24
Python成绩统计作业复盘:从平行列表到字典与函数重构
第二次作业发下来的那天课程群里比平时安静了不少。第一次作业只要照着课堂示例把代码拼一拼第二次题目一出大家才发现要面对的是一整套流程录入、存储、计算、排序、统计、输出。这道成绩统计题几乎在每个Python入门课程里都会作为第二次作业出现看起来只是for循环加列表加if的拼装但真正卡住大多数人的从来不是语法而是怎么把一段需求描述拆成能运行的程序结构。这篇文章算是一份复盘笔记把我当时的第一版实现、被指出的问题、以及最后重构的完整思路按顺序放出来。适合正在学Python、刚写完类似作业、或者作业能跑但自己心里不太有底的朋友对照着看。我不会只给结论每个改动为什么做、坑在哪里、怎么验证都会讲清楚。1. 这是一道什么作业成绩统计题的常见套路与考核重点1.1 题目长什么样我拿到的版本是这样的编写一个成绩统计分析程序从控制台输入班级人数 n。依次录入每位学生的姓名和三科成绩语文、数学、英语成绩为整数。计算每位学生的总分与平均分。统计班级各科的平均分、最高分、最低分。按总分从高到低输出学生名单。按总分划分等级优秀总分≥270、良好≥240、及格≥180、不及格180并统计各等级人数。不同课程的题目会有变体有的把三科改成五科有的要求最后输出到文件有的干脆让你做一个带菜单的小系统。但核心骨架完全一致循环录入一批记录对每条记录做聚合计算再对整批数据做统计和排序。把这道题吃透后面写爬虫清洗数据、写脚本处理日志、写工具解析配置文件本质上都是同一套思路。1.2 表面在考语法实际在考流程设计第一次作业通常只考你能不能敲对代码变量怎么命名、print怎么写、if的冒号在哪。第二次作业的考核重心突然变成了流程设计——你可不可以把一句话需求拆成一个个明确的执行步骤然后为每个步骤选择合适的数据结构和控制结构。具体来说这道题至少牵涉以下知识点循环与输入range(n)的边界input()和类型转换的配合数据组织多个平行列表、列表套字典、还是直接上对象聚合计算sum()、max()、min()的使用平均分用/而不是//排序按总分排序时的key函数怎么写reverseTrue意味什么条件分支多级if / elif / else的边界划分区间不能重叠也不能漏掉格式化输出print(f{name}{score})小数位控制中文冒号和空格的对齐问题。我见过很多同学把第一题拿满分却在第二题上卡了两天。卡住不是因为不会写for循环而是不知道先声明一个空列表然后往里面追加数据这种操作模式。这种模式一旦建立起来后面接触文件读写、网络请求返回的数据处理都会顺畅很多。2. 第一版实现按直觉写代码先把流程跑通2.1 拿到题目先拆步骤我的习惯是不急着开编辑器。先把题目里的话翻成伪步骤写在草稿纸上读班级人数 n。重复 n 次读姓名、语文、数学、英语存起来。遍历所有学生算每个人的总分和平均分输出。分别对三科计算平均、最高、最低输出。把学生按总分降序排序输出排名。统计四个等级的人数输出。这个步骤清单看起来很简单但它逼着你想清楚一件事数据在每一步之间是怎么流动的。录入完成后数据放在哪计算时从哪里取排序后又如何保持关联想明白了这些再写代码就是翻译工作。2.2 第一版代码长什么样第一次写的时候我没有用任何花哨的语法老老实实开了四个列表来存数据。当时的想法是一个人有姓名和三科成绩那就每个属性一个列表索引对齐就行。n int(input(请输入班级人数: )) names [] chinese_list [] math_list [] english_list [] for i in range(n): name input(f请输入第{i 1}位学生姓名: ) chinese int(input(语文成绩: )) math int(input(数学成绩: )) english int(input(英语成绩: )) names.append(name) chinese_list.append(chinese) math_list.append(math) english_list.append(english) print(\n学生个人汇总:) for i in range(n): total chinese_list[i] math_list[i] english_list[i] print(f{names[i]}总分{total}平均分{total / 3:.1f}) print(\n班级各科统计:) for subject, scores in [(语文, chinese_list), (数学, math_list), (英语, english_list)]: avg sum(scores) / n print(f{subject}平均分{avg:.1f}最高{max(scores)}最低{min(scores)}) order sorted(range(n), keylambda i: chinese_list[i] math_list[i] english_list[i], reverseTrue) print(\n总分排名:) for i in order: total chinese_list[i] math_list[i] english_list[i] print(f{names[i]}{total}分) level {优秀: 0, 良好: 0, 及格: 0, 不及格: 0} for i in range(n): total chinese_list[i] math_list[i] english_list[i] if total 270: level[优秀] 1 elif total 240: level[良好] 1 elif total 180: level[及格] 1 else: level[不及格] 1 print(\n等级人数:, level)这段代码是能跑的当时的输出也符合预期。我拿三个学生试了一下一个295分、一个250分、一个150分排名和等级都正确。交之前觉得自己挺厉害后来才发现能跑只是最低标准。2.3 跑通之后先别急着交现在回头看当时最欠缺的是测试意识。我跑的都是正常数据完全没有想过输入abc会发生什么没有想过如果班级只有1个人会不会出问题也没有想过录入负数成绩到底算不算bug。建议你拿到任何作业都先做这三组测试用例类型输入示例预期行为正常数据3人分数各异排名、平均分、等级都正确边界数据1人最高分最低分平均分程序不崩异常数据成绩输入abc应该提示重新输入而不是直接报错第一版代码在第三组测试上必挂。int(input(...))遇到非数字会直接抛ValueError程序终止。这个问题说大不大说小也不小但它揭示了代码在真实环境里最脆弱的点输入是用户给的不是你想当然的那样子。3. 代码审查这版作业能得几分哪里会被打回去重写我把第一版代码拿给身边一个写代码多年的朋友看他的评价就三句话平行列表用多了主流程太长输入处理太脆弱。这三句话把这份作业最大的三个问题全点出来了。3.1 问题一三组平行列表的隐患names、chinese_list、math_list、english_list四个列表并行靠索引保持一致。这种写法在数据量小的时候没问题但只要你做一次排序、插入或删除隐患立刻就冒出来。举个例子排序时我写的是sorted(range(n), keylambda i: chinese_list[i] math_list[i] english_list[i])拿到的是下标序列。后续所有读取都要带着这个下标去四个列表里取值。如果哪天你直接在names上调用.sort()而忘了同步排序其他三个列表数据就全错位了。更麻烦的是如果需求从三科改成五科你得同时维护六个列表哪怕只加一科也要在所有相关代码里补一遍。这种结构带来的认知负担远超它节省的几行代码。当一个概念学生的属性被拆散到多个变量里时你就失去了一次性操作一位学生的能力。3.2 问题二计算逻辑和输出逻辑全搅在一起第一版的代码是从上往下一条龙录入完立刻输出个人汇总然后算班级统计然后排序最后统计等级。表面看逻辑连贯实际上计算、判断、输出三件事完全耦合了。这意味着什么如果你想单独测试等级判断这一段逻辑——比如总分290应该算优秀、总分269应该算良好——你只能把整个程序从上到下跑一遍然后在控制台输出里找答案。如果你想改成把报告输出到文件而不是屏幕你得去改好几处print。如果哪天排序规则发生变化你又要反复定位到底是哪一行在控制排名。专业的做法是把程序拆成几个职责清晰的函数录入函数只负责录入计算函数只负责计算输出函数只负责输出。每个函数都能单独测试组合起来也清晰。这是第二次作业真正想教的东西只是当时我不懂。3.3 问题三输入不做校验异常随便崩第一版代码里没有任何防御措施。int(input(...))是裸奔的用户如果手滑输入七十八或者直接敲回车程序当场崩溃。更隐蔽的问题是负数成绩能录进去超过100分的成绩也能录进去逻辑上根本说不通。你可能觉得我自己用的程序我怎么会输错。但作业一旦交出去评分的人不会按你的剧本输入。真实世界里也一样脚本会被别人拿去跑会被奇怪的输入锤爆。养成任何外部输入都不可信的习惯是这份作业能给你的另一层训练。这里的解决方案很简单用while循环反复询问直到拿到合法的整数。4. 重构升级从能用到好用的迭代路径4.1 用字典组织学生数据第一版里学生是一个人但在代码里他是四个列表里的四个元素。重构的第一步就是让他回到一个整体。最自然的方式就是字典student { name: 张三, chinese: 95, math: 88, english: 92, }一个学生就是一个字典多个学生放在一个列表里students [] for i in range(n): student {} student[name] input(f请输入第{i 1}位学生姓名: ) student[chinese] get_score(语文) student[math] get_score(数学) student[english] get_score(英语) students.append(student)这里有一个我真实踩过的坑字典的创建必须放在循环内部。我第一次重构时把student {}写在了循环外面结果循环里每轮都在改同一个字典students列表里看似追加了三个学生实际上三个元素都指向同一个被反复覆盖的字典——最后打印出来的所有学生都是最后一个人的数据。这背后的原理是Python的引用语义列表里存的是对象的引用不是拷贝。排查时我用id()函数打了个位置才发现三个元素的地址完全一样。这个坑如果你遇到了八成也是同样原因。从平行列表换成字典之后原来sorted的写法也顺理成章地清晰了def total_score(student): return student[chinese] student[math] student[english] ranked sorted(students, keytotal_score, reverseTrue)key接收的是一个函数这个函数从学生字典里取出总分排序时按这个值比较。每个元素都自带所有数据排序后根本不用担心索引错位。4.2 把核心逻辑拆成函数函数是对逻辑的封装也是给代码画分界线。重构后的结构是这样的get_students(count)负责循环录入学生数据返回students列表total_score(student)给定一个学生字典返回他的总分class_stats(students)返回各科平均分、最高分、最低分的字典rank_students(students)返回按总分降序排列的新列表level_count(students)统计各等级人数print_report(students)负责所有格式化输出。拆完之后主流程短得惊人def main(): n get_positive_int(请输入班级人数: ) students get_students(n) ranked rank_students(students) stats class_stats(students) levels level_count(students) print_report(ranked, stats, levels)你不需要一次看懂所有函数每个函数是一个独立的小世界。想改输入逻辑只动get_students。想改等级判定只动level_count。想验证排名单独调用rank_students传入一条假数据就行。这种一处需求一个入口的体验和第一版那种牵一发动全身的感觉完全不同。4.3 再加三道保险校验、文件读写、异常处理输入校验是防御式编程的第一步。我给成绩输入写了一个小工具函数def get_score(subject): while True: raw input(f请输入{subject}成绩: ).strip() try: score int(raw) except ValueError: print(输入的不是整数请重新输入。) continue if 0 score 100: return score print(成绩应在0到100之间请重新输入。)有人可能觉得while True配continue的写法很绕但它完美解决了输入不合法就要重新问这个需求。非法输入不会让程序崩溃而是退回循环起点重新等待。这是一套通用的输入模式学一次能用在所有项目里。文件读写是很多变体题目的硬性要求也是程序从演示工具变成可用工具的关键一步。用标准库里的json模块保存三行读取也三行import json def save_students(students, filenamestudents.json): with open(filename, w, encodingutf-8) as f: json.dump(students, f, ensure_asciiFalse, indent2) def load_students(filenamestudents.json): with open(filename, r, encodingutf-8) as f: return json.load(f)ensure_asciiFalse保证中文正常显示indent2让文件可读。存盘之后再加载回来所有字段和类型都是原来的样子。题目如果有下次运行时还能继续统计的需求这两个函数直接就能顶上。4.4 升级后的完整代码把前面几段拼起来去掉过于啰嗦的注释长这样import json def get_score(subject): while True: raw input(f请输入{subject}成绩: ).strip() try: score int(raw) except ValueError: print(输入的不是整数请重新输入。) continue if 0 score 100: return score print(成绩应在0到100之间请重新输入。) def get_positive_int(prompt): while True: raw input(prompt).strip() try: value int(raw) except ValueError: print(请输入整数。) continue if value 0: return value print(人数必须大于0。) def get_students(count): students [] for i in range(count): student {} student[name] input(f请输入第{i 1}位学生姓名: ).strip() student[chinese] get_score(语文) student[math] get_score(数学) student[english] get_score(英语) students.append(student) return students def total_score(student): return student[chinese] student[math] student[english] def rank_students(students): return sorted(students, keytotal_score, reverseTrue) def class_stats(students): n len(students) result {} for subject in [chinese, math, english]: scores [s[subject] for s in students] result[subject] { avg: sum(scores) / n, max: max(scores), min: min(scores), } return result def level_count(students): level {优秀: 0, 良好: 0, 及格: 0, 不及格: 0} for s in students: total total_score(s) if total 270: level[优秀] 1 elif total 240: level[良好] 1 elif total 180: level[及格] 1 else: level[不及格] 1 return level def print_report(students, stats, levels): print(\n总分排名:) for i, s in enumerate(students, 1): print(f第{i}名{s[name]}总分{total_score(s)}) print(\n各科统计:) subject_names {chinese: 语文, math: 数学, english: 英语} for key, value in stats.items(): print(f{subject_names[key]}平均{value[avg]:.1f}最高{value[max]}最低{value[min]}) print(\n等级人数:, levels) def save_students(students, filenamestudents.json): with open(filename, w, encodingutf-8) as f: json.dump(students, f, ensure_asciiFalse, indent2) def main(): n get_positive_int(请输入班级人数: ) students get_students(n) ranked rank_students(students) stats class_stats(ranked) levels level_count(ranked) print_report(ranked, stats, levels) save_students(ranked) if __name__ __main__: main()这份代码比第一版长了但每一段都承担单一职责。你可以在某个函数里替换实现不会引爆全局。真要说还缺什么就是缺那一套测试用例过一遍以及把class_stats里的中文科目映射抽到常量里。5. 从这份作业延伸出去提交自查清单与后续练习方向5.1 提交前五分钟的自查清单写完代码和真的把作业交出去之间还差一道自查。我把后来工作中养成的检查习惯带回到这份作业上整理成一张表检查项自查方法为什么重要异常输入输入abc、负数、100以上程序应提示重试而不是崩溃边界人数试 n1单条记录时平均分、最高分、最低分要自洽数据结构是否还有平行列表同一概念的数据应聚合到字典/对象函数边界主流程是否超过一屏过长说明逻辑没有拆分到位命名可读变量名能否望文生义一个代码只写一次读很多次输出格式平均分是否统一小数位数字对齐影响报告的专业感文件刷新重新运行能否从JSON恢复验证保存和读取是否对称把这张表过一遍比你多跑十遍正常数据都管用。尤其是异常输入那项很多人交作业前完全没想过用户会不按规矩来。5.2 这份作业值得反复重写的几个方向第二次作业的价值不在于交上去拿一个分数而在于你可以反复重写它来检验自己在不同阶段的理解。我后来陆续用这同一个题目做了下面这些变体每次都收获不小换CSV文件存储把students.json换成students.csv学一下csv模块的读写顺便理解表格数据怎么在文本文件里表达。加可视化用matplotlib把各科平均分和等级人数画成柱状图把控制台程序变成带图表的分析工具。做成交互菜单录入、查询、修改、删除、统计、退出一个菜单循环包住所有功能。这就需要重新设计main()的结构也会逼着你考虑修改学生信息之后统计结果如何同步更新。引入对象把学生从字典升级成一个简单的dataclass或类把total_score变成方法。这能提前让你感受面向对象的设计思路。写单元测试把level_count和rank_students抽出来用pytest写几组断言验证边界值。测试代码有时候比业务代码更能训练逻辑。按我的经验把同一道题目用五种方式重写比连做五道新题目更管用。因为你在同一个领域里反复打磨能清楚看到每种设计方案的代价和收益。5.3 一些来自实战的碎碎念如果只让我说一句话总结第二次作业我会说代码能跑只是起点结构清晰才是目标。第一次作业你可以拼凑第二次作业你必须开始设计。当时我觉得把四个列表换成一个字典列表是小题大做后来在真实项目里处理上百万条数据时才发现数据结构的选择决定了一个脚本的上限在哪里。最后一个建议是交完作业别急着删保存好第一版和重构后的版本。过一个月再回来看这份代码你会清晰地看到自己进步的速度。Python学习路上最好的老师往往就是几天前的自己——因为那时候的代码到处都是现在能一眼看穿的破绽。