1. 别急着写代码,先想清楚这份作业到底在考什么
拿到“Python第二次作业”这个题目时,很多同学的第一反应是:终于要写代码了,赶紧打开编辑器试一试。但第二次作业往往不是第一次作业那样“打印个九九乘法表、算个闰年”的热身题了,它开始真正区分“学过语法”和“能写程序”这两件事。我见过不少人第一次作业满分,第二次作业却改到怀疑人生,原因不是题目难,而是根本没搞清楚这次作业背后的考核意图。
第二次作业的典型指向是结构化编程。也就是说,老师不再满足于你从上到下写一串能跑出结果的脚本,而是希望看到你开始用函数组织代码、用文件保存数据、用异常处理应对不合法输入。如果你拿到手的题目里涉及“学生信息管理”“成绩统计”“通讯录”“图书登记”之类的小系统,那基本可以确定考核点就是这几个:流程控制是否熟练、函数封装是否合理、数据是否能持久化到文件、程序遇到意外输入时会不会崩。
我当年带过的项目中,有一大半同学在第二次作业丢分都丢在同一类问题上:不是功能没实现,是代码只在自己写的“标准输入”下能跑,一旦换个调换格式、输入一个空行、文件里多了一个逗号,程序就当场阵亡。这份作业表面上是写一个小管理系统,实际上是在训练“健壮性”和“拆解能力”这两样东西,而这两样恰恰是后边做任何实际项目都绕不开的基本功。
还有一点需要提前说:如果你拿到的是某某平台上的作业,不要试图以“能提交通过”为唯一目标。提交通过只是及格线,真正有价值的做法是把它当作一个迷你项目去打磨。因为第二次作业之后,第三次、第四次作业的复杂度通常会阶梯式上升,你现在偷的懒,后面都得加倍还。
2. 从分析题目开始:这版学生信息管理系统到底要管什么
2.1 第一次作业和第二次作业之间,难度跃迁在哪里
第一次作业大概率是让你输出一些计算结果,或者用if-else处理几个判断分支。第二次作业命题时,老师通常会引入“多数据 + 重复操作 + 文件落盘”这三件套。以最经典的学生信息管理为例,它的核心需求通常包含这四块:
- 添加学生记录:录入姓名、学号、成绩等信息
- 查询/浏览记录:按学号查一个人,或者列出全部
- 修改/删除记录:更正错误数据,或者移除已离校学生
- 保存到文件,下次启动能重新读入
看起来功能不多,但每一条都踩在“结构化程序设计”的关键节点上。你把代码写成一个平铺直叙的大循环也能跑通,但当你试着把每个功能拆成独立函数时,就会体会到模块化的好处:主程序只负责调度,每个函数只管一件事,出问题的时候定位极快。
2.2 我建议你先把数据结构定下来,而不是先想界面
很多人写这类作业时有个坏习惯:先琢磨“用户按1做什么、按2做什么”,急着搭菜单,结果写到一半发现不知道用什么变量存数据。倒过来做会顺畅很多——第一步先确定数据怎么组织。
学生这类实体,最自然的方案是用字典。一个学号对应一个包含姓名和成绩的字典:
student = { "id": "2025001", "name": "张同学", "score": 88 }多个学生就是字典的列表,或者用学号做外层字典的键:
students = { "2025001": {"name": "张同学", "score": 88}, "2025002": {"name": "李同学", "score": 92} }两种结构都能用,区别在于:列表适合顺序遍历和保持插入顺序;字典适合按学号随机查询,查找时不用写循环。我个人更推荐第二种,也就是以学号为键的字典结构,因为第二次作业里“按学号查找、修改、删除”是高频操作,字典直接一步到位,而列表需要每次遍历。你写代码省事,老师审查代码时也会觉得你思路清楚。
成绩的处理上,不要只存一个总分,尽量把结构设计得有点层次感。比如语文、数学、英语单独存,再提供一个计算平均分和总分的逻辑,这样后面如果题目要求排序会非常方便。
students = { "2025001": { "name": "张同学", "scores": {"语文": 90, "数学": 85, "英语": 88} } }2.3 搞清楚这版作业的“最小可运行版本”长什么样
如果你现在完全没有头绪,就先别想那些花哨的排序、统计、图表展示功能。先把最小可运行版本做出来,这个版本只包含以下内容:
- 程序启动时,尝试从文件读取已有数据
- 进入一个while循环,打印菜单选项
- 用户输入1-5的数字,分别对应添加、查询、修改、删除、退出
- 每次操作结束后,把内存中的数据写回文件
- 输入不合法的选项时提示用户重新输入,而不是直接崩溃
这个骨架一旦跑通,你的第二次作业就已经完成70%了。剩下的工作就是在这个骨架上加细节:异常处理、输入校验、数据格式化。别一上来就想做得完美,先让主流程转起来,再逐层加固,这是写任何程序都适用的节奏。
2.4 为什么我建议你把文件格式定为JSON而不是CSV
保存数据到文件是第二次作业的关键得分点。常见的做法有三类:纯文本按行写、CSV逗号分隔、JSON结构化存储。纯文本最省事但解析麻烦,比如你把“张同学,88”写进文件,下次读的时候split(",")一下还能用,但一旦成绩变成三门,字符串里再混入中文逗号,你就知道什么是人间地狱了。
CSV比纯文本规范一点,但它在处理嵌套结构时依然别扭——姓名、多门成绩拼成一行容易出错,而且python自带的csv模块需要额外学一套读写接口。JSON是这三种里最适合这门作业的:Python字典和JSON格式几乎是——对应的,json.dump和json.load两个函数就完成了全部序列化工作。很多同学会说“老师没教过JSON”,但这恰恰是第二次作业隐含的野心:让你学会查文档、学会使用标准库中以前没见过的东西,这种能力比背几十个语法点值钱得多。
import json def save_to_file(students, filename="students.json"): with open(filename, "w", encoding="utf-8") as f: json.dump(students, f, ensure_ascii=False, indent=4)注意两个参数:ensure_ascii=False保证中文写入文件后还是人话,indent=4让文件排版漂亮,方便你调试的时候直接打开看。
3. 代码骨架怎么搭:把需求翻译成函数列表
3.1 主循环、功能函数、工具函数三层拆分
确定了数据结构和文件格式后,就可以设计函数了。我不太建议把所有逻辑塞进一个超大while循环里,虽然那样能跑,但是一旦出bug,你会陷入“从头读到尾也找不到问题”的痛苦。把功能拆成函数后,每个函数独立测试,问题范围瞬间缩小。
一个清晰的主循环大概长这样:
def main(): students = load_from_file() while True: show_menu() choice = input("请选择操作: ").strip() if choice == "1": add_student(students) elif choice == "2": query_student(students) elif choice == "3": modify_student(students) elif choice == "4": delete_student(students) elif choice == "5": save_to_file(students) print("再见!") break else: print("无效选项,请重新输入")这里有个细节值得强调:每次操作后保存,还是退出时统一保存?两种策略各有道理。统一保存在“程序意外崩溃”时会丢失所有修改;每步都存虽然多一点磁盘读写,但对这种小体量的学生数据而言完全不是问题,换来的是更安全。我建议采用每步都存,理由很简单——第二次作业判分时助教可能会强制中断程序或乱按Ctrl+C,如果数据没及时落盘,最后的分数可能很难看。
3.2 添加学生时,输入校验比“能存进去”重要十倍
添加学生是整套系统里最常被调用的功能,也是最容易暴露输入问题的环节。学号空了怎么办?成绩输入了英文字母怎么办?分数超过100怎么办?这些问题不加校验,程序不会立刻崩,但它会在后续的查询、排序、统计中向你索债。
写一套基础校验并不复杂,用异常处理配合循环,确保每项输入合法后才写入数据结构:
def add_student(students): student_id = input("请输入学号: ").strip() if student_id in students: print("该学号已存在,请使用修改功能") return name = input("请输入姓名: ").strip() if not name: print("姓名不能为空") return scores = {} for subject in ["语文", "数学", "英语"]: while True: raw = input(f"请输入{subject}成绩: ").strip() try: score = float(raw) if 0 <= score <= 100: scores[subject] = score break else: print("成绩必须在0到100之间") except ValueError: print("请输入数字") students[student_id] = {"name": name, "scores": scores} save_to_file(students) print("添加成功")这里有几个工程上的习惯值得你慢慢养成。第一,输入后立刻.strip()去掉首尾空格,避免用户手一抖多敲了个空格导致判断失灵;第二,成绩转换用float(raw)而不是int(raw),因为成绩可能有小数;第三,每个查询、添加操作都做“存在性检查”,要么直接拦截,要么给出明确提示,绝不能静默覆盖。
3.3 查询、修改、删除三个操作里最容易被忽视的边界情况
查询看着简单,按学号查一次,输出信息即可。但别忘记一个场景:查一个不存在的学号。如果不加判断,程序会报KeyError,而KeyError对用户来说就是“莫名其妙的崩溃”。修改和删除同理,先检查学号是否存在,再做操作,这种防御式编程是第二次作业的重要得分点。
删除功能还有个隐藏的坑:删除后要把学生记录从字典里弹出,但不能在遍历字典的同时修改字典——如果你用for循环遍历字典并从中删除元素,Python会直接抛RuntimeError。正确写法是直接用pop(学号, None),第二个参数是默认值,不存在时返回None而不是报错:
def delete_student(students): student_id = input("请输入要删除的学号: ").strip() removed = students.pop(student_id, None) if removed: save_to_file(students) print("删除成功") else: print("未找到该学号")修改功能则拆成两步:先找到记录,再修改其中的字段。修改时建议把“可修改的字段”限定在姓名和各科成绩上,学号不要改——因为学号是字典的键,改学号本质上得先删旧键再建新键,处理起来容易出错,而且现实中学生的学号也不会频繁变化。
3.4 给代码加上最简单的“人味”:让提示信息和输出排版可读
作业的验收常常是人眼检查加运行测试,输出可读性直接影响观感。存数据时用了JSON,打印时也得把数据格式化得漂亮一点。别直接print一个字典,那输出挤成一坨,老师看着头疼,你自己调试时也慢。可以用循环逐行打印,或者构造一个简单表格:
def print_student(student_id, info): print(f"学号: {student_id}") print(f"姓名: {info['name']}") total = sum(info["scores"].values()) avg = total / len(info["scores"]) for subject, score in info["scores"].items(): print(f" {subject}: {score}") print(f" 总分: {total:.1f} 平均分: {avg:.1f}"):.1f是格式化小数位的写法,保留一位小数,平均分不会出现一长串无意义的小数尾数。这个输出不算高级,但足够清爽,评委一眼就能看清有没有实现“统计总分平均分”这个加分需求。
4. 文件读写这关,藏着第二次作业80%的失分点
4.1 文件不存在的处理:第一次启动时别让程序当场崩溃
第一次运行程序时,数据文件还不存在。如果直接调用json.load(open("students.json")),FileNotFoundError会让程序当场退出,这对用户来说可是灾难体验。正确的做法是判断文件是否存在,不存在就返回空字典:
import os def load_from_file(filename="students.json"): if not os.path.exists(filename): return {} with open(filename, "r", encoding="utf-8") as f: try: return json.load(f) except json.JSONDecodeError: print("数据文件损坏,将使用空数据启动") return {}4.2 文件损坏这种极端情况,怎么用异常处理优雅兜底
上面代码里我顺手加了一个json.JSONDecodeError捕获。这个异常处理是什么场景?就是数据文件被人为改过、或者上次写入时程序被强行终止导致文件内容不完整。对第二次作业来说,这个try-except可能显得“过度设计”,但它确实能说明你对异常处理的理解已经超过及格线。老师看到这段代码,大概率会认为你具备处理真实项目问题的意识,而不仅仅是为了交差。
在真实工作场景里,文件读写几乎每一步都可能出错:磁盘满了、权限不足、编码不对、文件被其他地方占用。把这些异常都捕获并且在程序层优雅处理,是“能用的代码”和“好用的代码”之间的一道重要分水岭。当然第二次作业不用全都处理,但至少把文件不存在和JSON解析失败这两类最典型的场景覆盖掉,这个思路已经足够。
4.3 防止中文写入乱码的关键设置encoding="utf-8"
这个坑几乎每个写文件的新手都会踩一次。Windows系统上,如果不显式指定编码,open默认的编码可能是GBK;如果你在macOS或Linux上写文件时用了UTF-8,换到Windows上打开文件,中文就会变成一串乱码。
解决方式是在所有open()调用中都显式写上encoding="utf-8"。这不是可选的“加分项”,而是必须养成的习惯。第二次作业中涉及文件读写的同学里,因为乱码被扣分的不在少数。你写得规范,不仅是给自己省事,也是在向审阅者传递一个信号:你知道编码问题的存在并采取了防范。
5. 把数据“玩起来”:从基础功能到拉高分的排序与统计
5.1 平均分排序:看似简单,但要照顾到分数数据结构变化
如果题目允许你发挥,或者你想让第二次作业的完成度显得更有诚意,加一个排名功能是性价比很高的选择。按平均分从高到低排序,核心代码用Python内置的sorted加key函数即可:
def rank_students(students): ranked = sorted( students.items(), key=lambda item: sum(item[1]["scores"].values()) / len(item[1]["scores"]), reverse=True ) for i, (student_id, info) in enumerate(ranked, start=1): avg = sum(info["scores"].values()) / len(info["scores"]) print(f"第{i}名: {info['name']} ({student_id}) 平均分 {avg:.1f}")这一小段代码展示了三个知识点:sorted函数的key参数如何使用、lambda表达式、enumerate的起始索引用法。如果你在第二次作业里能写出这样的代码,说明你对Python的“函数式风格”已经有初步触觉了,这在同学中是挺亮眼的表现。
5.2 单科成绩最高分/最低分:一道5行代码但考察细心的小题
统计单科最高分和最低分也是常见加分项。实现思路很简单:遍历所有学生,取出某门科目的成绩,用max/min函数求极值。但有个细节:有的学生可能缺考某一科,如果该科成绩不存在,访问时就会KeyError。所以你需要在取值时用get()方法并给一个默认值,或者提前过滤掉没有该科成绩的学生:
def subject_stats(students, subject): scores = [info["scores"][subject] for info in students.values() if subject in info["scores"]] if not scores: print(f"暂无{subject}成绩数据") return print(f"{subject}最高分: {max(scores)}") print(f"{subject}最低分: {min(scores)}")注意列表推导式里那个if subject in info["scores"],这就是防御式编程的体现。成绩记录可能不完整,程序不能因为某条数据缺失就罢工。
5.3 成绩区间筛选:如何用几个if条件拼出一个“不笨”的查询
再往外延伸一点,你可以做一个“按分数区间筛选学生”的功能。传入最低分和最高分,返回所有平均分在这个区间内的学生。这个功能写起来不复杂,但能把你对循环、判断、数据提取的综合运用能力展示出来,而且它天然适合作为第二次作业的扩展题“查询成绩在80到90之间的学生有哪些”的解法。
def filter_by_score_range(students, low, high): results = [] for student_id, info in students.items(): avg = sum(info["scores"].values()) / len(info["scores"]) if low <= avg <= high: results.append((student_id, info["name"], avg)) return results这些扩展功能加不加,取决于你的时间和精力。我的建议是:如果基础功能已经稳定,加一个排序和一个区间筛选,会明显提升作业的完成度和区分度,也让期末的综合成绩有更多缓冲空间。
6. 实测中翻车的三个经典场景:排查链路完整复盘
6.1 场景一:程序一运行就FileNotFoundError,文件到底去哪了
这是最高频的翻车现场。现象:运行python main.py,程序直接抛文件不存在的异常。新手的第一反应往往是“我明明写对了啊”,然后开始怀疑人生。
排查过程是这样的。先看程序的工作目录:os.getcwd()。如果你在A文件夹里启动了Python,但代码里写的是相对路径students.json,那这个文件是相对于A文件夹而言的。很多同学习惯双击运行脚本,但脚本所在目录和当前工作目录不一致的时候,相对路径就会找错位置。解决方式有两条:一是把数据文件路径改为使用绝对路径,不过这不太优雅;二是在读写文件之前,先把工作目录切到脚本所在目录:
import os base_dir = os.path.dirname(os.path.abspath(__file__)) filename = os.path.join(base_dir, "students.json")但第二次作业如果只是让老师用命令行运行,直接用相对路径也没问题。关键是要理解:相对路径是相对于“当前工作目录”而不是“脚本所在目录”,理解这一点比修好这个bug本身更有价值。
6.2 场景二:输入一个“abc”成绩,程序疯狂报错退出
输入非法字符导致崩溃,是第二次作业里第二高频的翻车点。现象:用户输入成绩时手抖打了“abc”,程序抛出ValueError,直接终止。
排查思路是回到异常处理上。float("abc")会抛ValueError,而ValueError没有被任何try捕获,程序自然跑飞。修复方式就是前面代码里展示的while循环加try-except:只要转换失败就提示重新输入,直到拿到合法数字。这个小逻辑其实和你日常使用App的体验是一致的——你输错了,App不会退出,而是告诉你重新输入。把这种“人机交互”思路移植到命令行程序里,就是一个合格的交互设计。
6.3 场景三:数据写入文件后中文变乱码,怎么看都是“锟斤拷”
乱码问题我们在前面提过,根源就是编码不一致。现象:在Windows上写入中文后重新读取,发现文件里全是“锟斤拷”或者类似火星文。排查链路很简单:查写入时用的open()有没有指定encoding="utf-8";查读取时用的open()有没有指定encoding="utf-8";如果你的代码里有些地方写了有些地方漏了,混乱就在所难免。规范做法是把所有打开文件的入口都统一指定UTF-8编码,不要依赖系统默认值。如果一次程序里有多处读写,干脆定义一个统一函数:读文件用load_from_file,写文件用save_to_file,其他代码不直接碰open()。
6.4 这三个坑这么容易踩,背后的共性是什么
三个场景看似独立,实际上指向同一个核心问题:程序不能对用户的输入和运行环境做过于乐观的假设。文件可能不存在、输入可能非法、编码可能不匹配——这些都是异常情况,而健壮的程序恰恰是在异常情况下仍能保持可用。把这种“悲观假设”内化成写代码的本能之后,你写出的每一个程序质量都会有质的提升。第二次作业最大的价值就在于此:它用一个小系统,逼着你提前体验真实项目中最常见的那几类“意外”。
7. 验收之前,做一遍你自己的“检测清单”
写完代码不是结束,验收才是真正见真章的时刻。我强烈建议在提交之前,至少走一遍下面这份检测清单,你可以把它保存下来,以后每次做项目都能用得上:
- 程序在数据文件不存在的情况下能否正常启动
- 连续添加多个学生后,重启程序数据是否还在
- 成绩输入负数、大于100、纯字母时程序是否提示错误而不是崩溃
- 查询、修改、删除一个不存在的学号时提示是否友好
- 菜单选择输入“9”或其他非法选项时是否提示重试
- 退出再启动后,文件里的中文是否显示正常,无乱码
- JSON文件手动打开后,数据是否缩进清晰、结构可读
- 所有
open()调用是否都指定了encoding="utf-8" - 主逻辑是否拆成了函数,而不是一坨冗长的顺序代码
- 模块顶部的注释或docstring是否写清楚了程序功能和使用方式
如果你逐条过完这些检查,仍然没有发现致命问题,那这份第二次作业的完成度就已经超过大多数同学了。别小看这份清单,我见过有些同学代码写完了但从没重启过程序验证过持久化功能,结果就是功能在单次运行里全对,一重启全丢光,最后得分惨淡。
做检测时还有一个心得:把自己当成一个什么也不懂的用户去用这个程序。不要按照你写代码时的理想输入来操作,而是故意输入一些奇怪的值——空格、零、超长文本、带表情符号的字符串。程序能在这些“恶意”输入下保持稳定,才算真正意义上的完成。
8. 第二次作业之后,怎么把这次经验“滚”成第三个项目的能力
第二次作业的结束点,其实是你编程能力起飞的一个拐点。如果你完整地把这个学生信息管理系统写完并调通,那么你实际上已经掌握了构建几乎所有小型命令行应用的核心套路:持久化存储、结构化数据、交互循环、异常处理。这三个半技能,是后续写爬虫、写数据分析脚本、写自动化工具的底层能力。
我自己的体会是,做完第二次作业后,可以尝试做一个小小的“升级挑战”:把原来的命令行交互换成带简单界面的版本,或者把存储从JSON换成数据库表格。这种增量式练习比“重新学一遍语法”效率高得多。它们会让你在不知不觉中理解“如何在一个已有系统上做扩展,而不破坏原有功能”——这个能力在真实开发里比记住某个库的API重要得多。
如果你还能再进一步,试着给代码加上类型注解(type hints)和基本的函数注释:
def add_student(students: dict) -> None: """添加新学生记录,并保存到文件。"""这个习惯看似不起眼,但它会让你的代码在半年后回看时仍然容易读懂。写代码和写文章一样,清晰永远是第一位的。第二次作业教给你的不只是一个JSON怎么读写,而是一种“面向健壮性和可维护性去设计”的思维方式。把这次作业当成一块跳板,而不是一个通关任务,你会走得更远。