最近又有一批人卡在Python第五次作业上。前四周还能跟着示例代码敲一敲,到了这一周突然变成“代码每一行都看得懂,真让自己从头写就不知道从哪儿下笔”。这个现象太典型了。第五次作业基本意味着课程从单一语法演示进入综合应用阶段,函数、数据结构、文件读写这些内容要揉在一起用,考察的不再是你记住了哪个关键字,而是你能不能拿Python解决一个具体问题。这篇文章就拿一个非常有代表性的第五次作业题目来讲,把设计思路、核心代码、常见坑全部过一遍,适合正在被作业折磨,或者自学到函数阶段想找个完整项目练手的朋友。
1. 第五次作业到底在考什么
1.1 课程进度与这次作业的定位
第五次作业在大多数Python课程里是一个分水岭。这个时间点,变量、类型、输入输出、条件分支、循环都已经讲完了,很多老师还开了函数和简单数据结构的头。这次作业的核心目的,是把这些知识点串起来做一个小项目。你会在这一周第一次真正面对“问题分解”这件事:一个完整的小系统需要哪些功能,每个功能怎么独立成函数,数据怎么在函数之间传进去、传出来。这和之前写几十行脚本完全是两种心态。
很多同学觉得作业难,不是某个语法不会,而是上手就把所有代码堆在同一个流程里,while循环套if,写了两百行以后自己都看不懂。这是没有建立“函数思维”的典型症状。可以这么说:第五次作业能不能做顺,直接决定你后面学面向对象、学项目开发时会不会继续被动挨打。
1.2 一个典型的第五次作业长什么样
各校题目虽然五花八门,但基本逃不过这几个方向:学生成绩管理、通讯录管理、图书借阅登记、商品库存记录。就拿出现频率极高的“学生成绩管理系统”来说,常见要求是这样的:
- 能添加学生信息和成绩;
- 能查看所有学生,并计算总分、平均分;
- 能按成绩从高到低排序;
- 能查找、修改、删除学生;
- 程序关闭后数据不丢,下次打开还能看到。
第5条是真正的分水岭。前面几条用列表加循环硬写也能凑合,但“数据不丢”意味着必须碰文件读写。很多学生写菜单写得挺顺,一到持久化就不知道从哪下手,最后只能每次启动手动重新录一遍数据。这篇文章的核心代码就是按照这套需求来写的,你换成通讯录、图书登记、商品库存,思路完全一样,改个字段名就行。
2. 动手前必须想明白的几个知识点
2.1 为什么非用函数不可
如果你打算用一个while循环加几百行if解决整个作业,我劝你趁早停下来。函数不是用来给老师看的摆设,它是用来降低复杂度的。想一下,添加学生、修改成绩、删除学生,这三件事都需要“按名字找到这个学生”。如果每个功能里都写一遍遍历列表、比较名字、找到了怎么处理,程序会膨胀得非常厉害,而且将来改一个查找逻辑要改三处,很容易漏改。
正确做法是把“按名字找学生”抽成一个函数,三个地方都调用它。这是函数的第一个价值:消除重复代码。第二个价值是隔离问题。每个函数只负责一件事,出错的时候你能快速定位是文件读写的问题,还是业务逻辑的问题。第三个价值是方便测试。写完 add_student 可以直接在命令行里调一下,确认逻辑正确,再去做菜单交互,不必每次测试都从头跑一遍完整流程。
2.2 用列表还是字典存储数据
这个选择第一次真正影响作业质量。一个学生自然应该是一个字典:{"name": "张三", "math": 90, "english": 85}。为什么不用列表 ["张三", 90, 85]?因为下标没有语义。你写 parts[1] 的时候,根本不知道它是数学还是英语,代码可读性差,后面加字段还容易串位。写 stu["math"] 就一目了然。
所有学生放进一个列表里。列表天然支持按顺序遍历、用索引定位、用sort排序。这就是Python里“列表 + 字典”最经典的数据组合,后面学爬虫、做数据处理也一直在用这个模式。有些作业如果规定用二维列表,本质上差别不大,只是取数据时从 stu[1] 变成 stu["math"] 而已。但能主动用字典做的,老师印象分会好很多。
这里要提前埋一个重点:列表是可变对象。你把一个列表传进函数,在函数里 append 一个元素,外面的列表真的会多一个元素。很多同学一开始不放心,非要在函数里 return students,其实在Python里,直接修改传入的列表本身就是有效的。至于什么时候不用return、什么时候必须return,第四章我会讲清楚。
2.3 文件读写的三条铁律
作业要求“关闭程序后数据不丢”,本质就是用文本文件做持久化。文件操作从第一次用就开始养习惯,后面能少踩很多坑。我给你总结三条铁律。
第一,用 with open(...) 打开文件,不要手动 open 然后 close。with 代码块结束之后文件会自动关闭,哪怕中间抛异常也不会泄漏文件句柄。第二,读写时统一指定编码 encoding="utf-8"。Windows 下如果你不指定,默认可能是 GBK,在本机好好的文件,拷到别的电脑很可能变成乱码,或者直接报 UnicodeDecodeError。第三,读出来的内容全部是字符串,要参与数学计算前必须先转成 float 或 int。这条看着简单,却是第五次作业的重灾区。
保存格式我用最简单的一种:每行一个学生,字段用英文逗号分隔。姓名和成绩里几乎不会出现逗号,解析起来不容易出错。如果以后字段里真的可能含逗号,再引入 csv 模块处理,现阶段这个方案够用且直观。
3. 从零写一个能交作业的版本
3.1 搭出骨架:数据结构与读写文件
一般我会建议从数据层开始写,先把文件的读写搞定,再做增删改查,最后写菜单。菜单只是外壳,核心是数据且数据能保存。先设定文件名,再写读文件函数:
import os FILE_NAME = "students.txt" def load_data(): if not os.path.exists(FILE_NAME): return [] students = [] with open(FILE_NAME, "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue parts = line.split(",") students.append({ "name": parts[0], "math": float(parts[1]), "english": float(parts[2]) }) return students这段代码里有几个细节必须说清楚。第一步先判断文件是否存在,不存在直接返回空列表。如果不判断,open 一个不存在的文件会抛 FileNotFoundError。第二步,line.strip() 用来去掉行尾的换行符,不然 split 之后最后一个字段会带着 \n。第三步,读出来的成绩字符串要转换成 float,这就是类型转换,也是第五次作业里很多人崩溃的地方。我在这里用 float 而不是 int,因为成绩很可能有 85.5 这种小数,用 float 更稳妥。
对应的写回函数也很简单:
def save_data(students): with open(FILE_NAME, "w", encoding="utf-8") as f: for stu in students: f.write(f"{stu['name']},{stu['math']},{stu['english']}\n")这里直接遍历列表,每个学生写一行。拼接格式必须和 load 时的 split(",") 完全对应:存的时候用逗号,读的时候就按逗号拆。两边约定一致,数据就能稳定读写。f-string 是Python 3.6以后引入的写法,现在课程普遍用3.10以上,可以放心用。
3.2 实现增删改查:函数之间怎么协作
接下来是核心操作。我习惯先写“查”,因为添加、修改、删除都要用到“按名字找学生”。查的基础版本:
def find_student(students, name): for stu in students: if stu["name"] == name: return stu return Nonefind_student 遍历列表,找到名字匹配就返回字典本身,找不到返回 None。返回字典本身这一点很关键,因为修改和删除都要拿到这个字典对象才能继续操作。
添加学生最简单:
def add_student(students, name, math, english): students.append({"name": name, "math": math, "english": english})修改学生成绩:
def modify_student(students, name, new_math, new_english): stu = find_student(students, name) if stu is None: return False stu["math"] = new_math stu["english"] = new_english return True这里我直接修改 stu,而 stu 指向的是 students 列表里那个字典对象,所以外层的列表同步变化,不需要把新的列表返回出去。这就是前面提到的“字典是可变对象”的实际应用。如果你在这里又写了一句students = [...],那反而是在函数里创建了一个新列表,外面不会变。
删除需要使用索引才能 pop,所以单独处理:
def delete_student(students, name): for idx, stu in enumerate(students): if stu["name"] == name: students.pop(idx) return True return False这一组函数都很短,但协作关系很清晰。find_student 被 modify 和 delete 内部调用,只写了一次。将来如果要求改成按学号查找,只需要改 find_student 一个地方,其他调用点不用动。这种设计思路的价值,到后面写几百行的项目体会更深。
3.3 主循环与交互:小系统的大门面
数据层和业务操作都备齐了,最后用主函数把它们串起来。重点不是语法,而是“用户输入和函数参数怎么对接”。菜单我建议做成最简单直观的版本,while True 循环加 if/elif,不要搞花架子:
def main(): students = load_data() while True: print("\n===== 学生成绩管理系统 =====") print("1. 添加学生") print("2. 查看学生") print("3. 修改成绩") print("4. 删除学生") print("5. 按总分排序") print("6. 统计平均分") print("0. 退出") choice = input("请选择:").strip() if choice == "1": name = input("姓名:").strip() math = float(input("数学成绩:")) english = float(input("英语成绩:")) add_student(students, name, math, english) save_data(students) elif choice == "2": if not students: print("暂无数据") else: for stu in students: total = stu["math"] + stu["english"] print(f"{stu['name']},{stu['math']},{stu['english']},总分{total}") elif choice == "3": name = input("要修改的学生姓名:").strip() new_math = float(input("新的数学成绩:")) new_english = float(input("新的英语成绩:")) if modify_student(students, name, new_math, new_english): save_data(students) print("修改成功") else: print("找不到这个学生") elif choice == "4": name = input("要删除的学生姓名:").strip() if delete_student(students, name): save_data(students) print("删除成功") else: print("找不到这个学生") elif choice == "5": students.sort(key=lambda x: x["math"] + x["english"], reverse=True) save_data(students) print("排序完成,选2可查看结果") elif choice == "6": if not students: print("暂无数据") else: avg_math = sum(s["math"] for s in students) / len(students) avg_eng = sum(s["english"] for s in students) / len(students) print(f"数学平均分:{avg_math:.2f},英语平均分:{avg_eng:.2f}") elif choice == "0": print("已退出,数据已保存") break else: print("无效输入,请输入0-6的数字") if __name__ == "__main__": main()这个主循环的逻辑很简单。用户选1,程序就接收三个输入,调 add_student 加进列表,再马上 save_data 写进文件。用户选3,先调 modify_student,它返回 True 才保存并提示成功,返回 False 就提示找不到人。每个分支都是“输入 -> 处理 -> 保存”,结构非常统一。
运行效果大概是这样:第一次启动没有文件,添加几个学生后退出,目录下自动生成一个 students.txt。第二次再启动,数据自动加载进去,不用重新录入。把 3.1 到 3.3 的函数按顺序拼到一个 student_system.py 文件里,就是一个能直接交作业的版本。
4. 最容易扣分的十个坑(排查实录)
4.1 input()拿到的是字符串:类型转换的坑
热搜榜里“python类型转换”被搜爆不是没道理。Python 的 input 不管用户输入什么,返回的都是字符串。你要拿它做算术,必须先转类型。第五次作业里最常见的报错,就是输入成绩后计算总分时报 can't multiply sequence by non-int of type 'float',翻译过来就是“你把字符串和数字混着算了”。比如:
math = float(input("数学成绩:"))如果输入 90,这里会得到 90.0。如果忘了 float(),得到的是字符串 "90",后面做加法时就会出问题。另外 int() 和 float() 有区别,成绩可能有 85.5 这种小数,所以存成绩建议用 float。如果老师明确要求整数分,那可以用 int(),但要在输入前说清楚,避免分数带小数。
4.2 排序用sort还是sorted:一行解决十行难题
“按成绩排序”几乎是第五次作业标配。很多老师还没来得及讲 lambda,学生们就开始手写冒泡排序,几十行代码绕来绕去,最后边界条件还写错。实际上 Python 的 list.sort() 自带按 key 排序的能力:
students.sort(key=lambda x: x["math"] + x["english"], reverse=True)意思很直接:把每个学生按“数学加英语”的和进行排序,reverse=True 表示从大到小。就这一行。如果你对 lambda 不熟,也可以写一个具名函数:
def total_score(stu): return stu["math"] + stu["english"] students.sort(key=total_score, reverse=True)两种写法效果一样。sorted() 和 sort() 的区别是,sorted 返回一个新列表、不修改原列表;sort 直接修改原列表、返回 None。在这个系统里,排序之后往往还要保存,所以直接用 sort 更自然。老师要是问起来,你还能说得清这两者的区别,本身就是一个加分项。
4.3 文件编码、换行和路径
我检查作业时最常看到的报错是 UnicodeDecodeError: 'gbk' codec can't decode byte ...。这个问题的根源就是 Windows 中文环境默认用 GBK 编码读取文件,而文件本身是按 UTF-8 写的。解决办法就是统一在 open 的时候指定 encoding="utf-8",读写两头都保持一致,问题直接消失。
换行符的坑也常见。用 with open 按行读文件时,每一行末尾都会自带一个换行符。如果不处理,split 之后最后一个字段会带着 \n 进入数据,看着没差别,其实已经脏了。所以读取时先 line.strip(),这是我在 load_data 里这么写的原因。
还有一个路径问题。有的同学为了图省事,直接在代码里写死绝对路径,比如 C:/Users/xxx/Desktop/作业/students.txt。作业交到老师电脑上,路径不存在,程序直接崩。正确做法是使用相对路径 "students.txt",文件会生成在程序运行目录下,简单可靠。
4.4 边界条件:空数据、找不到人、除零
第五次作业最容易让程序当场崩溃的,往往不是复杂逻辑,而是三个边界条件。第一个是空列表求平均分。len(students) 为 0 的时候做除法,直接抛 ZeroDivisionError。第二个是修改或删除一个不存在的学生。find_student 返回 None,不判断就直接 stu["math"],会抛 TypeError: 'NoneType' object is not subscriptable。第三个是成绩录入不校验范围,-5 分、120 分也照单全收,老师可能不直接判错,但追问一句“你怎么保证数据合理性”,很多人就答不上来。
我整理了一张速查表,排查的时候对照着看:
| 场景 | 报错信息 | 正确处理 |
|---|---|---|
| 空列表求平均分 | ZeroDivisionError: division by zero | 先判断 if not students |
| 找不到学生就修改 | TypeError: 'NoneType' object is not subscriptable | 先判断 find_student 结果是否为 None |
| 输入非数字成绩 | ValueError: could not convert string to float | 用 try/except 捕获异常,再提示重新输入 |
| 读取文件编码错误 | UnicodeDecodeError: 'gbk' codec can't decode | 读写都加上 encoding="utf-8" |
| 数据行有空行 | ValueError: not enough values to unpack | 跳过 strip 后为空的行 |
4.5 代码规范和交作业的小细节
不少老师在第五次作业开始认真检查代码风格。变量名用 name、math,别用 a、b、c;函数名写 add_student、save_data,别用 f1、f2。还有一个很容易被忽视但很加分的入口写法:
if __name__ == "__main__": main()这种写法允许这个文件既可以直接运行,也可以被其他代码导入而不自动执行主流程。虽然第五次作业基本不会用到模块导入,但这是正规项目的标准入口,写上去会让老师觉得你有工程意识。
交报告的时候,不要直接把整个代码复制粘贴就完事。至少写三句话:这个程序有哪些功能、用了什么数据结构、自己实现了哪个改进点。不用长篇大论,但这三句话能说明你是真理解自己的代码,而不是网上找了一段改个标题就交上来。
5. 环境没配好?先解决这个再谈作业
5.1 快速确认你的Python环境
我知道有一部分同学卡在第五次作业,根本不是代码问题,而是程序压根跑不起来。如果你不清楚自己电脑有没有装 Python,最简单的办法是打开命令行窗口,输入:
python --version能打印出 3.x 版本号,说明环境没问题。如果提示“python 不是内部或外部命令”,那就是没装,或者装了也没把 Python 加入 PATH。这种时候不需要慌,去官网下载安装包重装一遍,安装向导第一步把“Add Python to PATH”勾上,之后命令行就能找到 python 了。
还要确认你用对了解释器版本。现在课程基本都用 Python 3,不要再写 Python 2 的 print 语法。Windows 下命令通常是 python,Linux 和 Mac 下可能是 python3。如果在命令行能识别,但编辑器里运行报错,那十有八九是编辑器选错了解释器,请看下一节。
5.2 VS Code里跑代码的正确姿势
VS Code 写 Python 是目前比较主流的选择。新建一个 .py 文件后,代码右上角一般会出现一个三角运行按钮。第一次运行前,VS Code 会提示你选择解释器。按 Ctrl+Shift+P,输入“Python: Select Interpreter”,选到你机器上实际安装的那个 Python。
常见的两个问题。第一个是右下角弹出“没有安装 Python 扩展”,自己去扩展商店搜 Python,装微软官方那个。第二个是命令行能运行 python,但 VS Code 运行时报 No module named xxx,这通常是选错了解释器,比如你电脑上装了多个 Python 版本,VS Code 选到了另一个。解决方式就是重新选择解释器,看清楚路径,和命令行里 python --version 对应的环境保持一致。
环境问题本质就是“解释器 + 插件 + 工作目录”三件事。理顺之后,90% 的无法运行问题都能解决。如果换了 PyCharm 也一样,Community 版对课程作业完全够用。
交作业之前,我还想多说一句。根据我给同学查作业的经验,第五次作业只要做到四件事就能稳过:程序能从开头跑到结尾,数据关掉程序再打开还在,修改和删除时有“找不到学生”的提示,代码里没有写死自己电脑的绝对路径。这四条过了,剩下的基本就是分数高低问题,而不是过不过的问题。
我自己的习惯是写完这种小系统,一定会把添加、修改、删除、排序、统计、退出这几个动作完整跑一遍再交。别觉得老师不会每个按钮都点一遍,一个边界bug很可能就是你差那0.5分的理由。等这版作业交完,你可以再自己加一个 try/except 异常处理,把成绩录入时的非法输入挡住,或者把数据改成 JSON 格式保存。第五次作业只是个起点,往后学面向对象、做爬虫、分析数据,这一套“函数拆分 + 数据结构 + 文件持久化”的思路会一直跟着你走,现在多花半小时把底层逻辑吃透,比四处找源码抄一份要值得多。