说实话,看到“python第五次作业”这个标题,我第一反应就是又到了学期中段那种让人又爱又恨的节点。前四次作业可能还在练变量、循环、条件判断,到了第五次,老师通常就开始要求你把前面学的东西串起来,写一个像样的完整程序。我当年接到的第五次作业是一个学生信息管理系统,题目本身不算难,但它特别适合用来检验你对函数、文件、异常处理和数据结构到底掌握到什么程度。如果你也正好卡在这次作业上,或者只是想看看一个真实项目里Python代码是怎么组织起来的,这篇内容应该能省你不少事。
1. 作业内容整体拆解:第五次作业到底想考你什么
1.1 题面背后的隐藏考点:函数拆分与代码组织
第五次作业常见的形式就那么几种,要么是学生/图书/商品信息管理系统,要么是一个带菜单的小程序,也有老师会布置贪吃蛇或“人狗大作战”这类小游戏。但不管题目怎么变,核心考察点其实是同一件事:你能不能把一个稍微完整的需求,拆成函数去实现。
前几次作业里,你可能已经习惯了从上到下写一段脚本,跑完就拉倒。但到了这个阶段,程序开始有菜单、有循环、有数据保存,如果还把所有逻辑堆在同一个while循环里,代码很快就没法看了。我记得当时班里有个同学,一个文件写了三百多行,全在主函数里平铺,加一个功能就要在main()里加好几层if,最后改得满头大汗。
所以拿到题面之后,第一步不是急着敲代码,而是先看它的功能清单。以学生信息管理系统为例,核心功能大概就四个:添加学生、删除学生、查找学生、显示所有学生,再加一个退出。每个功能天然对应一个函数,这是最基础也最合理的拆分方式。你甚至可以先把这些函数名写在注释里,比如add_student()、remove_student()、search_student(),再往里面填代码,整体思路会清晰很多。
另外,这道作业的另一个隐藏考点是数据结构的选择。很多新手第一反应是用两个列表,一个存姓名,一个存成绩,然后用索引对应。这样写勉强能做,但一旦要删除中间某个人,两个列表的下标就对不上了。我建议直接使用列表套字典的结构,每个学生是一本“小档案”,例如{"id": "001", "name": "张三", "score": 88},所有学生再装进一个大列表。这样增删改查都只针对一个字典操作,代码逻辑简单,后面要加字段也方便。
1.2 技术选型思路:为什么文件存储选JSON而不是CSV
很多同学的疑问是,为什么不用CSV?这得看你的需求。CSV的好处是可以用Excel直接打开,适合纯表格数据。但CSV的解析和写入需要你来处理逗号、换行、引号这些细节,虽然Python有内置的csv模块,但相比JSON还是多了一层概念。
JSON(JavaScript Object Notation)在Python里几乎是为“列表套字典”这种结构量身定做的。它能把整个列表直接序列化成一个字符串,写进文件;读文件的时候再反序列化回列表,类型完全不会丢。代码量非常少:
import json def save_data(students, filename="students.json"): with open(filename, "w", encoding="utf-8") as f: json.dump(students, f, ensure_ascii=False, indent=2) def load_data(filename="students.json"): try: with open(filename, "r", encoding="utf-8") as f: return json.load(f) except FileNotFoundError: return []用CSV不是不行,但意味着你要自己处理更多边界情况,比如某个字段里如果出现了逗号,要不要加引号。在这个阶段,JSON明显是更贴合“写完作业”的选择。而且你之后如果是往爬虫、接口、数据分析这些方向走,JSON基本是绕不开的,早晚都得学,这次作业正好是个简单的练习机会。
2. 核心代码一步步拆开讲
2.1 先搭框架:主菜单循环和入口函数
我不建议一开始就从第一个功能写起,更好的顺序是先把整个程序的“骨架”搭出来,也就是主菜单循环。这个循环决定了用户跟程序怎么交互,一般长这样:
def main(): students = load_data() while True: print("\n===== 学生信息管理系统 =====") print("1. 添加学生") print("2. 删除学生") print("3. 查找学生") print("4. 显示所有学生") print("5. 退出") choice = input("请输入你的选择(1-5):") if choice == "1": add_student(students) elif choice == "2": remove_student(students) elif choice == "3": search_student(students) elif choice == "4": show_all(students) elif choice == "5": save_data(students) print("数据已保存,再见!") break else: print("输入无效,请重新选择。")这个骨架有几个细节值得注意。第一,students这个列表在程序启动时通过load_data()加载进来,如果之前保存过数据,程序重新启动后还能接着用,这一下就把“数据持久化”的概念带出来了。第二,在循环内部,choice = input(...)拿到的永远是字符串,直接和"1"、"2"这些字符串比较,就不需要再做类型转换,也避免了输入非数字字符时int()抛异常的麻烦。第三,退出分支里,我先保存数据再break,确保用户改动的数据不会丢。
我给这个骨架再补充一句:main()函数这个写法,本身就是Python程序的标准入口。很多人刚开始不习惯写main(),直接平铺在全局,但一旦用了函数组织代码,都会习惯性地把启动逻辑塞进main(),然后在文件末尾写if __name__ == "__main__": main()。这样做的意义是,如果这个文件被其他模块import,不会一导入就立刻启动程序,只有当成脚本直接运行时才会执行。这个知识点很基础,但考勤、面试都爱问。
2.2 核心函数实现:增删改查和关键细节
骨架搭好之后,再往各个函数里填内容就舒服多了。先看添加功能。这里比较容易忽略的是“学号重复”的判断,如果系统里已经存在某个学号,你又插入同一条,数据就会乱。
def add_student(students): sid = input("请输入学号:") # 先检查学号是否已存在 for s in students: if s["id"] == sid: print("该学号已存在,添加失败。") return name = input("请输入姓名:") score = input("请输入成绩:") # 这里可以把score转成float,顺便做异常处理 try: score = float(score) except ValueError: print("成绩格式不正确,请输入数字。") return students.append({"id": sid, "name": name, "score": score}) print(f"学生 {name} 添加成功。")这里我用了try...except把成绩字符串转成浮点数。第五次作业的评分标准里,异常处理经常是加分项,因为老师在测试时一定会故意输入一个非数字,看看程序会不会崩。提前兜住这种异常,印象分能差不少。
删除功能的常见写法是“输入学号,找到就删,找不到提示”。注意,students.remove()这个方法要求你传入要删除的那个字典对象,所以得先用for循环找到目标元素,再调用remove()。还有个小细节,如果你在for循环里找到了目标,并且调用了删除,一定要马上return或者break,否则循环继续下去可能会出问题。更严谨一点,可以配合一个found标志:
def remove_student(students): sid = input("请输入要删除的学号:") for s in students: if s["id"] == sid: students.remove(s) print(f"学生 {s['name']} 已删除。") return print("未找到该学号的学生。")查找功能可以做得灵活一点,支持按学号精确查,也支持按姓名模糊查。模糊匹配用in判断就行,比如if keyword in s["name"],这样用户只输入半个姓名也能找到。显示所有学生就更简单了,直接遍历列表打印。如果列表为空,可以提示“暂无学生数据”,否则手动加个输出格式。
2.3 数据保存与读取:JSON序列化的完整代码
这一节其实在1.2里已经给过save_data和load_data的代码,这里重点讲一下文件操作里的几个坑。
第一,encoding="utf-8"这个参数一定要写。Python默认的文件编码在不同操作系统上不一致,Windows上有时默认是gbk,如果你不指定,写中文时可能报错,或者写进去之后读出来是乱码。第二,json.dump(students, f, ensure_ascii=False, indent=2)里的ensure_ascii=False特别关键。如果不加,中文会被编码成\u5f20\u4e09这样的Unicode转义序列,虽然文件能读回来,但用记事本打开看完全没法辨认。indent=2是让JSON文件有缩进,格式化之后阅读更舒服。
读取的时候,我用了try...except FileNotFoundError,因为第一次运行程序时文件压根不存在,这时直接返回一个空列表就好。这种“尝试打开,打不开就给个默认值”的写法,在后端开发里也常见,叫做“容错处理”。不要觉得这是偷懒,它恰恰是程序健壮性的体现。
3. 实际写作业时踩过的坑
3.1 文件路径相关:运行代码却报FileNotFoundError
很多人在自己的电脑上运行程序一切正常,切换了目录或者用PyCharm跑的时候,突然报错FileNotFoundError: [Errno 2] No such file or directory: 'students.json',一脸懵。这大概率是当前工作目录跟你以为的目录不是同一个。
这里要理解一个概念:代码里写的相对路径"students.json"是相对于“当前工作目录”的,而不是相对于代码文件所在目录。在终端里跑,当前工作目录通常是你敲命令时所在的目录;在IDE里跑,可能是项目根目录,也可能由IDE的运行配置决定。解决办法很简单:要么把路径写成绝对路径,要么在运行前先确认工作目录。最省心的做法是在开发阶段统一用绝对路径,或者干脆用模块动态获取当前文件目录,再拼接文件名:
import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DATA_FILE = os.path.join(BASE_DIR, "students.json")这样不管你从哪里运行代码,都能找到这个JSON文件。这个技巧放到真实项目里非常常用,越早习惯越好。
3.2 中文数据乱码或写入后变成\uXXXX
写入JSON文件时中文变成\u5f20\u4e09,或者读取时中文变成乱码,这是第五次作业里出现频率最高的抱怨。原因就是我在上一节提到的:写入时要加ensure_ascii=False,读写文件时都要带encoding="utf-8"。
还有一个细节是,如果你在Windows的记事本里打开生成的JSON文件,看到的中文是正常的,但用Excel打开CSV时又乱码了,那是另一码事,需要给CSV文件加utf-8-sig编码。这次作业如果用的JSON,就不太会遇到这种二次乱码。总之,记住一个原则:Python处理文件中文字符,统一在打开文件时显式指定编码,不要靠系统默认值。
3.3 函数修改列表却不起作用的迷思
这个问题是初学者最容易绕晕的点。比如你在add_student(students)函数里明明写了students.append(...),主流程里打印students,看起来也能看到新增的数据,你觉得没问题。可一旦你改成students = [...]重新赋值,比如想“重置学生列表”,往往会发现主流程里的列表根本没变。
原因在于Python的参数传递是“引用传递”,但函数内部如果对参数进行重新赋值,比如students = some_new_list,那就只是让函数内部的局部变量students指向了另一个对象,外部的原列表没受任何影响。而students.append()是对原来那个列表对象做“原地修改”,所以外部能看到变化。
为了理清这个概念,我建议你在第五次作业时养成一个习惯:尽量把数据作为参数传入,并且在函数内做原地修改或者返回新的结果,不要动不动用global声明全局变量。虽然第五次作业规模小,用global也能跑,但养成这个习惯后,往后写更大项目时能少吃很多亏。
3.4 常见问题速查表
| 问题现象 | 原因分析 | 解决办法 |
|---|---|---|
中文写入文件后变成\uXXXX | json.dump默认ensure_ascii=True | 写入时加ensure_ascii=False |
程序报FileNotFoundError | 相对路径指向了错误的工作目录 | 用os.path.dirname(__file__)拼接绝对路径 |
| 菜单输入字符串直接崩溃 | 对输入做了int()但没有异常处理 | 用字符串比较,或try...except包住转换 |
| 删除学生后,数据刷新后还在 | 删除操作只改了内存,没调用保存 | 退出前调用save_data(students) |
| 添加重复学号没有提示 | 缺少查重逻辑 | 遍历列表,先判断id是否已存在 |
| 函数内修改数据外部不变 | 参数被重新赋值,不是原地修改 | 对列表用append/remove,避免重新赋值 |
这张表里的问题,基本覆盖了我当年看到的、以及后来帮人改作业时遇到过的大部分情况。如果你写的是小游戏,而不是管理系统,同样适用,只是数据结构和菜单内容不同罢了。
4. 从第五次作业往外走:下一步玩法
4.1 给作业加一个可视化报告
如果这次作业里已经有了成绩这个字段,那加一个可视化报告是非常出彩的扩展。用matplotlib把成绩分布画成柱状图,或者用饼图展示不同分数段的人数占比,一下子就比普通同学的多了一个量级。
import matplotlib.pyplot as plt def show_score_report(students): scores = [s["score"] for s in students] plt.hist(scores, bins=5, edgecolor="black") plt.title("学生成绩分布图") plt.xlabel("分数") plt.ylabel("人数") plt.show()这段代码把学生的成绩列表提取出来,用直方图展示分布。做这个扩展时你反而会发现,自己得回头把数据结构再理一遍——因为要一次性拿到所有人的成绩,之前“列表套字典”的结构就很好用了。如果你用的是散落的多个列表,这一步会相当痛苦。
4.2 把命令行程序改成Web接口
第五次作业的另一个人气升级方向,是把命令行程序改成一个带网页界面的服务。用Flask或者FastAPI,把“添加学生”“删除学生”“查找学生”这些操作映射成HTTP接口,你就能从网页或者接口调试工具里直接调用。
这个方向的驱动力很直接:现在的爬虫、后端、数据分析都离不开API。你提前把数据增删改查封装成函数,再在Flask里调用这些函数,你会发现“业务逻辑”和“接口层”分离的思路,就是从这个规模很小的项目里开始的。比如add_student(students)本来是从命令行收输入,改成Web接口后,数据从请求体里拿,其他部分几乎不用动。
4.3 爬虫和数据分析的天然入口
也有人会问,这次作业跟爬虫有什么关系?关系其实很大。爬虫的核心就是“发请求、拿数据、清洗结构、保存结果”,最后一步保存结果时,你往往要把抓下来的数据变成列表套字典的结构,再写入JSON或CSV。如果你在第五次作业时已经熟练掌握了列表套字典和JSON读写,爬虫的数据落地环节就畅通了。
数据分析也是类似的逻辑。pandas读入的DataFrame,本质上也是有行有列的二维表,跟“学生列表”这种结构非常像。你想筛选出成绩大于90的学生、统计人数,这些都和这次作业里的查找、遍历逻辑同出一源。所以我一直觉得,第五次作业不是终点,它是你从“学会语法”走向“能干活”的真正分水岭。
我个人在写这个作业时体会最深的一件事是:先别急着敲代码,拿一张纸把“用户会怎么操作”这条线画出来。先输入什么、后输入什么、哪些数据要存、哪些数据要改,顺着这个流程走一遍,代码结构基本就出来了。我第一次没规划直接写,写到一半发现增删改查里逻辑乱成一团,后来重来一遍,先列菜单、再列函数、最后填实现,两个小时就搞定了。如果你也卡在“到底从哪下手”,不妨试试这个方法。第五次作业真的不难,难的是静下心来把思路理清楚。