Python学生成绩管理系统作业复盘:函数、字典与JSON实战
看到“pyhthon第五次作业”这个标题我第一反应是又一个把“python”打成“pyhthon”的同学。不过说正经的这种带编号的作业题目在课程群里几乎每周都能见到而且每次问法都差不多“这个第五次作业到底要写什么”“有没有参考代码”。如果你也卡在这这篇复盘应该对你有用——它记录的是一次典型的Python函数与数据结构阶段综合作业核心任务是完成一个学生成绩管理系统涵盖字典、列表、文件读写、类型转换、lambda排序这些高频知识点。这次作业几乎能把之前学过的所有基础内容串起来也适合正在学Python教程、准备课程设计或想练手的人参考。我能理解为什么老师喜欢在这个阶段布置类似的题目单纯讲函数语法、字典操作太抽象真正动手写一个能增删改查、能保存数据的系统才算把语法变成了自己的东西。下面我按当时的设计思路到完整实现、调试踩坑、优化过程完整复盘一遍代码可以直接抄但更重要的是我写作业时想明白的那些“为什么”。1. 作业题目与现实需求拆解1.1 这份作业到底在考什么先说作业本身。按理说“第五次作业”这种命名本身没有信息量真正常见的情况是老师在作业系统里另附了具体要求。从常见教学进度判断第五次作业大多落在函数、字典、文件操作这个区间——正好是学完基础语法之后第一次做综合练习。我当时拿到的题目典型描述是实现一个学生成绩管理系统支持录入学生信息学号、姓名、至少两科成绩支持按学号查询、删除、修改学生信息支持显示全部学生和统计平均分数据需要能保存到文件程序重启后数据不丢很多同学看到这种题目第一反应是“这也太简单了”但真动手写就会发现里面藏着大量细节考点input读进来的是字符串你要不要转成float删除一个不存在的学号程序会不会崩保存数据用文本文件还是用JSON函数怎么拆才能让主程序看起来清爽这些恰恰是课程老师想通过作业考察的点。所以拆解需求时我习惯先把功能列成一个清单避免写着写着漏功能菜单循环显示用户输入数字进入对应功能添加学生记录输入学号、姓名、成绩显示所有学生记录按学号精确查找学生按学号删除学生按学号修改学生信息统计全班平均分、最高分按总成绩排序数据保存到文件启动时读取这样列出来之后整个程序的边界就清楚了后面就是逐个函数去填。1.2 技术方案选型为什么是“函数 字典 JSON”写这种作业最大的坑不是写不出来而是写到一半发现代码乱成一团。我记得班里有同学把所有逻辑全塞在用while循环里写了三百多行最后连自己都找不到哪里是入口。我这次从一开始就决定了三件事用函数封装功能、用列表套字典存数据、用JSON做持久化。为什么函数封装因为作业要求的功能点有八九个如果把每个功能直接平铺在main流程里代码会非常难读。函数的意义不是炫技而是把“干什么”和“怎么干”分开。main循环只负责接菜单指令、调用对应函数至于函数内部怎么处理数据不需要管。这样程序的结构是菜单循环一层功能函数一层数据操作一层。逻辑清晰也方便单独测试某个函数。为什么用字典存学生信息因为学生信息是典型的“多条属性”数据。每个学生有学号、姓名、成绩如果只用列表你得靠下标去猜第0个是学号还是姓名很容易混。用字典就变成了students[0][name]可读性和可维护性都强很多。至于整个数据容器选择列表套字典而不是字典套字典是因为排列顺序比较容易控制——列表天然保序append就是插入顺序用sorted排序之后也能按顺序遍历。为什么文件存储选JSON当时班里有人用txt手写格式比如每行存“1001,张三,88,90”读的时候再用split(,)拆开。这种方案能跑但很脆弱成绩是整数还好如果哪天有个字段里出现了逗号或者成绩是85.5解析就出问题。JSON格式自带类型表达能力json.dumps可以把字典和列表直接转成字符串json.loads又能还原成Python对象不需要自己写解析逻辑。可以少踩很多坑。1.3 运行环境准备这次作业我是在自己的电脑上用PyCharm完成的Python装的是3.10版本。如果你是第一次跑代码建议先确认两件事第一Python安装成功后能否在终端输入python --version看到版本号第二PyCharm里新建项目时解释器是否已经指向了正确的Python环境。当时同学群里最常见的问题就是“我能打开PyCharm但是运行按钮是灰的”基本上都是解释器没配好解决办法是在File - Settings - Project - Python Interpreter里重新选一次解释器就行。版本方面不需要纠结Python 3.8以上随便用代码里用到的语法没有依赖旧版本特性。文件操作和json都是标准库连pip install都不用直接跑。2. 数据结构与函数模块的整体设计2.1 数据模型别急着写代码先想清楚怎么存数据很多人写这种作业会犯一个通病打开编辑器就开始打代码打到一半发现数据存得别扭又回头改结构改完结构又要改所有函数。我的习惯是先花十分钟把数据模型定下来数据模型定了后面所有函数都围绕它写改动就很少。我选择的数据结构是students [ {sno: 1001, name: 张三, python: 88, math: 90, english: 85}, {sno: 1002, name: 李四, python: 92, math: 76, english: 88} ]最外层是一个列表students列表的每个元素是一个字典表示一位学生。字典的四个键分别是学号sno、姓名name、python成绩、math成绩、english成绩。为什么把学号作为字符串而不是整数因为学号如果以0开头整数会丢前导零而且学号本无数学意义不需要加加减减。这个细节在作业点评时被老师专门提过算是加分项。有的同学也会选择用字典套字典比如用学号当作外层字典的键students_dict { 1001: {name: 张三, python: 88, math: 90}, 1002: {name: 李四, python: 92, math: 76} }这种方案在“按学号查找”时其实更高效但有一个问题如果想要“按插入顺序列出所有学生”字典虽然在实际实现中也保留插入顺序但这样依赖“实现细节”的写法在通用场景下不严谨。而且用列表套字典写起来更贴近MySQL查出来的结果集——每一行是一条记录每一列是一个字段。考虑到作业后续还可能扩展比如支持批量导入、按字段筛选列表套字典的可扩展性最好。成绩我故意用float而不是int因为很多考试成绩可能有小数。如果用int存用户输入85.5时int(85.5)会直接抛异常这个问题后面在调试部分会详细说。2.2 函数模块怎么拆才清晰函数拆分的原则是“一个函数只干一件事”。我当时把整个系统拆成了十个函数每个都不超过二十行def menu(): # 打印菜单 def add_student(students): # 添加学生 def show_all(students): # 显示全部学生 def find_student(students): # 按学号查找 def delete_student(students): # 按学号删除 def update_student(students): # 修改学生 def get_avg(students): # 求全班平均分 def sort_by_total(students):# 按总分排序输出 def save_to_file(students, filename): # 保存到文件 def load_from_file(filename): # 从文件加载会不会拆得太细刚开始我也有点担心但后来发现这个担心是多余的。每个函数只管一个功能主程序和函数的边界非常清晰。要改某个功能只需要改一个函数不用翻动其他代码。这在写一百多行的程序时优势还不明显但如果作业要求继续加功能——比如加一个“按姓名模糊查找”你只需要再写一个函数然后在菜单里加一个分支就行。特别要说明的是add_student、delete_student这类函数我选择把students列表作为参数传进去。这样做的好处是这个函数是“纯操作”不依赖全局变量调试时可以单独导入模块手动调用非常方便。如果你有面向对象的基础当然也可以用类封装成StudentManager但面向对象不是这次的考察点用函数式写法最简单直接老师也更容易看懂。2.3 主菜单循环与入口设计写交互式的控制台程序核心是主循环。我当时的主循环长这样def main(): students load_from_file(students.json) while True: menu() choice input(请输入操作序号: ).strip() if choice 1: add_student(students) elif choice 2: show_all(students) # ... 其他分支 elif choice 0: save_to_file(students, students.json) print(数据已保存再见) break else: print(输入无效请重新选择)这里有几个设计上的细节值得说。第一choice直接以字符串形式比较而不是转换成整数。因为input本身就是字符串如果写成int(input())用户一旦输入了非数字字符程序立刻崩。直接比较字符串最多就是匹配不上进入else分支不会出异常。这是很多人忽略的小技巧。第二每次进入菜单前load_from_file加载一次退出时才save_to_file保存。这意味着内存中的students列表是整个程序运行期间唯一的数据源所有增删改查都在列表上操作最后统一写回文件。如果你在每个功能里都实时读写文件性能差不说文件还可能因为程序中途崩溃而写坏。第三菜单用函数打印而不是直接print。虽然看起来只是把几个print包进了一个函数但对主循环来说视觉上更清爽主函数只保留流程控制逻辑。3. 完整实现与关键细节解读3.1 数据初始化与文件读取先看文件读取部分。程序启动时第一个动作就是尝试从JSON文件加载历史数据。这里的核心思想是“容错”如果文件不存在不应该报错而应该返回一个空列表让程序正常启动。import json import os DATA_FILE students.json def load_from_file(filename): if not os.path.exists(filename): return [] try: with open(filename, r, encodingutf-8) as f: data json.load(f) if not isinstance(data, list): return [] return data except (json.JSONDecodeError, IOError): return []这段代码看起来简单里面有三层防护。第一层os.path.exists检查文件是否存在不存在直接返回空列表。第二层json.load可能因为文件格式损坏抛JSONDecodeErrorio操作可能抛IOError用try-except把这两个异常都接住统一返回空列表。第三层即使JSON解析成功也要检查解析出来的东西是不是列表防止有人手动编辑文件改成了字典。为什么这么谨慎因为程序的数据文件可能会被用户手动打开编辑。在作业场景下同学的文本编辑器可能保存成了带BOM的UTF-8或者中文标点不小心打成半角导致JSON格式错误。如果程序不做容错处理启动直接崩体验很糟糕。把这些判断写在前面后面所有功能函数都省心了。3.2 核心增删改查函数详解添加学生是整个系统的基础。它的逻辑是接收用户输入组装成字典加入列表。但这里有几个容易出错的点尤其是数据验证。我当时第一版写得很草率后来才逐步补全。def add_student(students): print(----- 添加学生 -----) sno input(请输入学号: ).strip() for stu in students: if stu[sno] sno: print(学号已存在添加失败) return name input(请输入姓名: ).strip() if not name: print(姓名不能为空) return try: python_score float(input(请输入Python成绩: )) math_score float(input(请输入数学成绩: )) english_score float(input(请输入英语成绩: )) except ValueError: print(成绩必须是数字添加失败) return students.append({ sno: sno, name: name, python: python_score, math: math_score, english: english_score }) print(添加成功)为什么要先查重因为学号是学生的唯一标识。如果允许重复学号后面按学号查询、删除时会同时操作多条记录逻辑就乱了。这个约束在需求里没写但作为系统设计的一部分是必须的。为什么成绩转换用float而不是int因为成绩可能是小数。用int会把85.5往下截断成85虽然不报错但数据就不准确了。float可以精确表示小数至少在一般场景下足够。如果担心输入了“abc”float(abc)会抛ValueError所以用try-except包住捕获到异常直接返回。查询和删除函数是天然配套的。查询只需要找到并打印删除需要找到之后从列表里移除。两者都依赖一个公共逻辑按学号找下标。def find_student(students): sno input(请输入要查找的学号: ).strip() for stu in students: if stu[sno] sno: print(stu) return print(未找到该学号) def delete_student(students): sno input(请输入要删除的学号: ).strip() for i, stu in enumerate(students): if stu[sno] sno: del students[i] print(删除成功) return print(未找到该学号)enumerate在这里的作用是同时拿到下标和元素。为什么删除用del students[i]而不是用remove因为remove需要传整个字典对象你还得先构造一个一模一样的字典才能删太麻烦而我们有下标del是直接按位置删更高效也更容易理解。修改学生信息的函数有一个设计难点是修改哪些字段最省事的做法是全部重新输入。但如果用户只想改成绩不想改姓名全量重输体验较差。我采用了“逐个字段选择”的方案用循环来控制修改流程def update_student(students): sno input(请输入要修改的学号: ).strip() for stu in students: if stu[sno] sno: print(当前信息:, stu) stu[name] input(请输入新姓名(留空不变): ).strip() or stu[name] try: new_py input(请输入新Python成绩(留空不变): ).strip() if new_py: stu[python] float(new_py) # math、english 同理 except ValueError: print(成绩输入有误本次修改终止) return print(修改成功) return print(未找到该学号)这里用了一个很常见的Python技巧input(...).strip() or stu[name]。如果用户直接回车input返回空字符串空字符串是Falseor就会取后面的stu[name]相当于保持原值。如果用户输入了新内容or取前面非空字符串就完成了修改。这个写法简洁但第一次看的人可能不太适应我写注释的时候特意标注了。3.3 显示、统计与排序用lambda一行搞定显示全部学生最朴素的方式是for循环逐行打印。但如果你想对齐格式用f-string加上宽度控制效果会好很多def show_all(students): if not students: print(暂无学生数据) return print(学号\t姓名\tPython\t数学\t英语) for stu in students: print(f{stu[sno]}\t{stu[name]}\t{stu[python]}\t{stu[math]}\t{stu[english]})这里用\t做简单对齐在控制台里看着还行但如果觉得列太宽窄不一可以改成f{stu[sno]:10}意思是左对齐宽度为10。这种格式化输出的细节在作业里不会单独考但显示效果直接影响老师批改时的第一印象。统计平均分和排序是这个作业里最有可能用到“高级一点”的语法的地方。计算全班平均分可以遍历所有学生的三科成绩累加再除以总人数def get_avg(students): if not students: print(暂无数据无法统计) return total 0 count 0 for stu in students: total stu[python] stu[math] stu[english] count 1 print(f全班平均分: {total / count:.2f})排序函数里就用到lambda了。如果你还不知道lambda是什么可以先把它理解为一个“一次性小函数”专门配合sorted这样的高阶函数使用。def sort_by_total(students): if not students: print(暂无数据) return sorted_list sorted( students, keylambda stu: stu[python] stu[math] stu[english], reverseTrue ) for i, stu in enumerate(sorted_list, start1): total stu[python] stu[math] stu[english] print(f第{i}名: {stu[name]} 总分: {total})key参数指定排序依据lambda stu: stu[python] stu[math] stu[english]这个表达式接收一个学生字典返回三科总分sorted内部会对每个学生调用它按返回值排序。reverseTrue表示从高到低。这可能是整个作业里最“值钱”的一行代码因为大多数人写到这只会手动写冒泡排序而你直接用sortedlambda既少写了几十行代码可读性还更高。3.4 文件保存JSON如何保证数据不丢网络热搜词里有一堆关于“python安装”和“python环境变量配置”的内容但程序写到这一步真正影响体验的往往是文件保存。我用json持久化的核心代码只有两行def save_to_file(students, filename): with open(filename, w, encodingutf-8) as f: json.dump(students, f, ensure_asciiFalse, indent2)ensure_asciiFalse很关键。这个参数让json.dump在写中文时直接输出中文而不是转成\u5f20\u4e09这样的Unicode转义序列。如果你不设这个参数保存后的JSON文件里中文全是乱码一样的东西虽然程序能读回来但用户直接打开文件看时会怀疑人生。indent2是让JSON文件里每个字段换行缩进方便手动查看和检查。保存时机的选择我选择了“每次修改数据后立即保存一次退出时再保存一次”。为什么要双保险因为程序可能在运行中崩溃如果只靠退出时保存中途崩溃会丢掉所有修改。但每次修改后保存也有开销如果数据量很大频繁写文件会影响性能。对于这次作业的数据规模每修改一次就保存完全没压力。在真实项目里通常会引入“脏标记”机制——只在数据被修改时保存这是后话。4. 调试踩坑与问题排查实录4.1 我实际踩过的三个坑先说排序后修改失效的问题。第一次写完sorted排序我发现按总分排序后去修改第一条记录的学号原先指向的数据没变。想了半天才反应过来sorted返回的是新列表里面的元素仍是原列表里的字典引用但列表顺序已经变了。如果我想让主列表students也跟着新顺序走应该把sorted的结果重新赋值给students或者在函数里写成students[:] sorted(students, key...)用切片赋值可以原地修改列表内容列表对象本身不会变但顺序会变。这个知识点不大但很能体现对Python可变对象的理解程度。老师在作业评语里特地批了“使用切片赋值原地修改列表理解到位”。第二个坑是input返回的字符串。用户输入“1001”后如果你拿它跟整数1001比较永远不相等因为一个是str一个是int。当时班上好几个同学卡在“按学号查询永远找不到记录”就是因为没有处理类型不一致。解决办法就是学号一律按字符串处理从输入到存储、到比较全程都是str不要混用。第三个坑是try-except的位置。我第一次写成绩输入时只把float(input(...))放进了try后面的字典构建放到了except外面导致一旦转换失败异常被捕获了但程序继续往下走把一条成绩字段残缺的数据append进了列表。这个bug很隐蔽因为程序不会崩但数据已经错了。正确的做法是一旦转换失败立即return后面添加学生字典的代码只允许在成功时执行。所以代码里我在except ValueError分支后面直接return再往下才是students.append。4.2 其他常见问题与排查速查表为了这份作业我把同学群里出现过的典型问题整理成了一张速查表如果你也遇到类似现象可以直接对照排查现象可能原因解决方法运行报ModuleNotFoundError: No module named json项目里创建了json.py文件覆盖了标准库删除自定义json.py或重命名为my_json.py保存后的文件中文乱码open没写encodingutf-8或json.dump没写ensure_asciiFalse打开文件和json.dump都加上encoding和ensure_ascii参数input输入成绩后程序崩溃用户输入了非数字int()或float()抛ValueError用try-except捕获ValueError修改学生数据却在文件里没变化只在退出时保存但程序中途崩溃每修改一次就调用save_to_file按学号删除没反应学号存在中英文空格差异输入后统一调用.strip()去空格第二次运行程序数据丢失文件读取路径不对比如当前工作目录不同用绝对路径或用os.path.join拼接按键1添加学生后直接退出主循环break放错了位置放在了if分支里检查break缩进确保它只在choice0时执行列表排序后原列表不变sorted返回新列表没有重新赋值使用students[:] sorted(...)其中“json.py覆盖标准库”这个坑值得单独说。有一次我为了保存数据自己写了一个json.py文件结果一运行就报错ImportError: cannot import name dump from json。原因是我自定义的json.py把标准库的json模块“遮蔽”了import json引入的是我自己的空文件。这个错误在刚学Python时特别容易遇到排查方法也很简单看看项目目录里有没有跟标准库同名的文件有就改个名。4.3 从“能跑”到“拿高分”的优化建议程序能跑只是及格线。我记得交作业前花了一个晚上专门做优化主要从四个方向入手。第一是异常处理全覆盖。用户输入不规范是常态不是在考试。成绩输入要能处理非数字菜单选择要能处理越界值文件读取要能处理文件损坏。这些健壮性设计不一定会写进需求文档但老师打开程序随便按两个键就能感受到差别。第二是函数注释。给每个函数写三个引号的docstring说明函数功能、参数、返回值。不需要很复杂一两行就够。比如def add_student(students: list) - None: 添加学生信息到列表。 :param students: 学生列表元素为字典 :return: 无 这既是作业态度也是职业习惯。后续接手的同学看你的代码或者你两个月后回来看自己的代码都会感谢这些注释。第三是数据验证前置。所有输入校验最好集中在函数入口处不要在代码中间散落。比如add_student里学号查重、姓名非空、成绩数字校验全部在构造字典之前完成一旦规则不满足立即return不要在中间状态里纠结。第四是代码风格统一。变量命名用snake_case函数名用动词开头布尔量避免在变量命名里用is_前缀。虽然作业不查代码风格但这决定了你写的是“一百行脚本”还是“一个小程序”。5. 作业之外的一点收尾把这次作业完整写完之后我最大的感受是课堂上学语法和理解语法的差距在一行一行敲代码的过程中会被迅速放大。以前我总觉得自己列表、字典、函数都会了但真到写一个系统时才明白每个知识点应该用在什么位置。函数不是装饰是用来拆解问题的字典不是数据容器是把现实事物建模的工具异常处理不是加分项是程序的基本礼貌。从这次作业开始我养成了一个习惯不急着写代码先在纸上把数据结构和函数清单列出来再动手实现。这个习惯后来帮我写数据分析项目、做爬虫、甚至做量化策略回测时都省了很多返工的力气。如果你也正在为类似的作业发愁不妨按这个思路试一次——先梳理需求再设计数据最后填充代码你会发现五个功能点的作业其实半天就能写完。