1. 第五次作业是个分水岭考的从记没记住变成会不会用1.1 前四次和第五次的本质区别python第五次作业这个标题乍看就是平平无奇的课程进度记录。但我这几年帮人改作业、带新人做练习对这个节点感触特别深它基本就是照着写代码和想着写代码之间的分界线。前四次作业考的大多是语法记忆。列表怎么切片、for循环怎么写、函数def完怎么调用、字符串有哪些方法这些本质上都是你记住了没有。哪怕完全不懂原理只要把课件里的例子改吧改吧也能交一份像模像样的上去。批改的时候我也能感觉到大部分人这时候还处于能跑就行、报错就懵的状态。到了第五次题目通常就变了一副面孔不再单独考某个语法点而是要求你把好几样东西串起来做一个小程序。最常见的就是字典、列表、函数、文件读写混合使用比如学生成绩统计、通讯录管理、文本词频统计。这一步最要命的地方在于没有任何人告诉你这题该用哪个数据结构你得自己判断、自己设计、自己把整个流程搭起来。我把前四次和第五次常见的差异整理成了下面这张表各位可以对号入座对比维度前四次作业第五次作业考察重点单个知识点是否掌握多个知识点能否组合题目提示通常会指明用什么基本不提示全靠自己选代码规模几十行以内往往上百行甚至更多失败后果改一处就能过一处出错全线崩盘核心能力记忆和理解拆解和设计很多人第一次挂科就挂在这次不是因为他笨而是他的学习方式还停留在记住语法阶段突然被要求设计系统自然手足无措。这篇文章我就拿一道非常典型的学生成绩统计题做例子把从数据分析到代码实现的完整思路走一遍顺便把批改作业时反复遇到的那些坑也一起说了。1.2 这次作业的常见配置字典 函数 文件读写我经手过的第五次作业不管题目怎么换花样内核基本就是三样东西的组合。第一是字典。因为你需要存储有结构的数据比如每个学生的姓名、学号、各科成绩。列表当然也能存但如果要做按学号查找某个人这种操作字典比列表高效得多代码写起来也更直观。第二是函数。代码量一旦过百行还全糊在main里面调试的时候你自己都会疯掉。老师要求拆函数不是为了让你好看而是为了让你能活到第六次作业。第三是文件读写。作业题目通常会要求把结果保存到文件或者从文件里读入数据再处理这一步能拦住一大半人——不是不会open和write而是编码、格式、异常这些细节根本没接触过。这三样东西单拎出来都不难难的是同时在同一个程序里用对。下面我就拿一个具体题目完整走一遍。2. 拿一道典型题目拆给你看学生成绩统计系统2.1 动手写代码前先把需求翻译成数据结构先看一道很典型的题目我改作业时遇到它的频率高得惊人编写一个学生成绩统计程序。支持录入若干学生的学号、姓名以及语文、数学、英语三科成绩计算每个学生的总分和平均分统计每门课的班级平均分、最高分、最低分最后按总分从高到低排序并把完整结果保存到文件中程序启动时如果文件存在还需要能够读出来展示。拿到这种题新手最容易犯的错就是撸起袖子直接写。写两行发现不知道数据往哪放然后开始胡搞最后交上来一份自己都跑不通的代码。我的建议永远是动手前先用三五分钟把题目拆成几件独立的事。这道题拆开其实只有五件事录入接收用户输入存成结构化数据计算每个学生的总分、平均分每门课的班级统计排序按总分降序排列保存把结果写入文件加载从文件读出并展示每一件事对应到Python里就是一个函数外加一个数据结构。你把题目拆到这里脑子里就有一张清晰的施工图了接下来只是逐个填空。2.2 字典怎么设键学号作键和列表存字典的取舍数据存储是整个程序的地基这一步选错了后面处处别扭。常见方案有两个我分别说下优缺点。方案A学号作为外层字典的键值是一个嵌套字典students { 2024001: { name: A同学, scores: {语文: 85, 数学: 92, 英语: 78} }, 2024002: { name: B同学, scores: {语文: 88, 数学: 79, 英语: 90} }, }方案B用一个列表列表里每个元素是一个字典students [ { sid: 2024001, name: A同学, scores: {语文: 85, 数学: 92, 英语: 78} }, { sid: 2024002, name: B同学, scores: {语文: 88, 数学: 79, 英语: 90} }, ]方案A的好处是按学号查人极其自然students[2024001]一下就出来了而且天然去重——同一个学号的键只会保留一个。缺点是字典本身没有顺序概念排序时必须先转成列表再做。方案B的好处是列表天生有序按总分排完序直接就是最终展示结果遍历也顺手缺点是按学号查找要写循环还得自己处理重复学号。这道题我实际推荐方案A。原因很简单录入时学号重复是一个非常现实的错误场景用学号当键字典自动帮你挡住了这个错误。至于排序问题sorted(students.items(), key...)一行就能解决完全不必为了排序牺牲去重能力。初学者容易犯的毛病是把展示顺序和存储结构混为一谈总觉得列表存进去什么顺序就什么顺序其实展示和存储本来就可以分开处理。2.3 输入模块的写法循环、去重、类型校验确定好数据结构第一个要写的函数就是录入。录入模块常见的要求是可以连续录入多个学生直到用户输入空行结束学号不能重复成绩必须是数字不能是abc这种东西。我给你看一个我比较满意的写法def input_students(): students {} while True: sid input(请输入学号直接回车结束).strip() if not sid: break if sid in students: print(学号已存在请重新输入) continue name input(请输入姓名).strip() scores {} for course in (语文, 数学, 英语): raw input(f请输入{name}的{course}成绩).strip() try: scores[course] int(raw) except ValueError: print(成绩必须是数字请重新输入该同学的完整信息) break else: students[sid] {name: name, scores: scores} return students这里有几个细节值得说。.strip()是用来去掉用户不小心打出来的空格2024001 和2024001如果不去空格就是两个不同的字符串查重就会失效。if sid in students这一句利用字典的键判重比自己在列表里循环找要简洁得多。还有那个try...except ValueError这是防止用户输入八十五或者8o这种非法内容直接把程序干崩。for...else这个结构很多初学者没见过它在循环正常走完没有被break打断时执行else里的内容刚好用来表示三科成绩全部录入成功。3. 函数封装为什么老师非要你把代码拆开3.1 一大坨代码的问题等你回头调试就明白了我改作业时见过不少千行英雄帖整个程序从头到尾一个函数全靠空行和注释分区。学生自己还觉得很自豪觉得一个文件搞定多酷。我真不建议这样干不是洁癖是这套写法在调试时是真的痛苦。想象一下你程序跑到第80行报了个错前面79行都在做录入、计算、排序混在一起的过程。你得一行一行读一边读一边在脑子里记账这个变量现在是啥这个列表被改过几次人的短期记忆容量很有限干这种事实在太吃力了。而如果你把功能拆成输入、计算、排序、保存几个独立的函数程序崩了之后你只需要想一件事这个报错发生在哪一步如果发生在保存那一步前面几百行都不用看了直接去查文件函数就行。另外独立函数还有一个隐藏福利你可以单独测试它。比如写完了course_stats立刻用一个写死的字典调用一下看看结果对不对不需要每次都得走完整个录入流程。这种小步快跑的验证方式是新手进阶非常重要的习惯。3.2 按职责拆函数输入、计算、排序、存取拿成绩统计这道题来说我建议至少拆成下面这几个函数def input_students(): # 返回 students 字典 ... def student_total(student): # 计算单个学生的总分 return sum(student[scores].values()) def course_stats(students, course): # 统计某门课的班级平均分、最高分、最低分 scores [s[scores][course] for s in students.values()] return { 平均分: sum(scores) / len(scores), 最高分: max(scores), 最低分: min(scores), } def ranked_list(students): # 返回按总分降序排列的列表 return sorted( students.items(), keylambda item: sum(item[1][scores].values()), reverseTrue ) def save_to_file(students, filenameresult.txt): ... def load_from_file(filename): ... def main(): ...为什么要单独写一个student_total而不是在排序和保存的时候各写一遍sum(...)因为重复代码是bug的温床。你需要改逻辑的时候比如突然要求平均分的计算方式变成去掉一个最高分再去掉一个最低分如果逻辑只写在一个地方改一处就行写了两三处你很有可能漏掉其中一处然后程序的表现就会变得非常诡异。ranked_list里的sorted我用了students.items()这样每个元素是(学号, 学生信息)的元组lambda 表达式里item[1]取到学生信息再sum(...)算总分。reverseTrue就是降序。这里有个小坑如果忘了reverseTrue默认升序全班第一名会排到最后一名去。3.3 可变默认参数一个隐蔽又经典的坑函数封装阶段还有一个坑几乎每届学生都会踩就是可变默认参数。看这段错误写法def add_student(name, scores[]): scores.append(score) # 假设这里要把成绩加进列表 return scores乍一看没什么问题scores默认给一个空列表调用时不传就用新的空列表。但实际跑起来你会发现第一次调用返回[85]第二次调用返回[85, 92]第三次[85, 92, 78]——所有的调用共享了同一个列表原因是Python函数的默认参数在定义时只计算一次之后每次都复用同一个对象。scores[]这个空列表在整个程序生命周期里始终是那一个列表并不是每次调用都新建。这在函数式编程里叫可变默认参数陷阱。正确写法是def add_student(name, scoresNone): if scores is None: scores [] scores.append(score) return scores用None当占位符每次函数被调用时如果没传参数scores就是None然后现场新建一个列表。这个方法你记不住原理也没关系记住可变对象列表、字典、集合不要做默认参数这条铁律就能避开绝大多数事故。4. 文件读写与异常处理让程序真正活到下次运行4.1 选什么格式保存txt、CSV 还是 JSON成绩统计这道题要求把结果保存到文件那文件用什么格式就值得权衡一下。这听起来像小题大做但格式选错了加载读回的时候能折磨死你。最朴素的做法是直接写txt每一行用逗号或者空格分隔字段。它的优点是简单直观记事本打开就能看缺点是没有结构自己解析时容易出错比如某个字段里意外含了逗号。稍微好一点的做法是CSV本质上是带格式约定的txtPython标准库里有csv模块读写都方便Excel也能直接打开。如果再讲究一点可以用JSON结构清晰json.dump和json.load一行搞定是三种里最省心的。考虑到这是课程作业题目如果没有明确要求我用CSV格式举例因为它正好能展示逐行解析这个能力点而且直观。下面是一个保存的示例def save_to_file(students, filenameresult.txt): with open(filename, w, encodingutf-8) as f: f.write(姓名,学号,语文,数学,英语,总分,平均分\n) for sid, info in students.items(): scores info[scores] total sum(scores.values()) avg total / len(scores) f.write( f{info[name]},{sid},{scores[语文]},{scores[数学]}, f{scores[英语]},{total},{avg:.1f}\n )注意我用了with open(...) as f这个写法能保证文件用完后自动关闭程序中途崩了也不会留下没关闭的文件句柄。如果你用老式写法f open(...)然后忘了f.close()程序运行次数多了Windows下会出现文件被占用这种莫名其妙的报错。4.2 编码问题UTF-8 和 GBK 的恩恩怨怨文件读写里拦路最多的问题就是编码。很多同学的代码在课堂上跑得好好的回家一跑中文字符全变成了乱码或者直接报错UnicodeDecodeError: gbk codec cant decode byte 0x...原因很简单Windows上Python默认的文本编码是GBK而很多代码和文件用的是UTF-8。写入的时候用了UTF-8读取的时候却按GBK解码自然就炸了。解决办法就是读写文件时永远显式指定编码别偷懒依赖系统默认值with open(result.txt, w, encodingutf-8) as f: ... with open(result.txt, r, encodingutf-8) as f: ...写入和读取都用同一套编码就不会出问题。你要是把文件发给别人、或者放到云笔记里UTF-8是最通用的选择跨平台基本不会出乱子。这里也顺带提一句如果你在Windows自带的记事本里打开UTF-8文件没乱码但程序读出来是乱码先检查是不是代码里少了encodingutf-8这个参数。4.3 文件不存在、内容损坏try/except 兜底最后一个常见需求是程序启动时如果文件存在就读取并展示。注意题目的措辞——如果文件存在。这意味着文件可能压根不存在尤其是第一次运行的时候。如果你直接open(result.txt, r)文件不存在时程序直接崩给你看。所以读取逻辑一定要做存在性判断和异常兜底def load_from_file(filenameresult.txt): students {} try: with open(filename, r, encodingutf-8) as f: next(f) # 跳过表头 for line in f: parts line.strip().split(,) if len(parts) 4: continue # 行格式不对跳过 sid parts[1] name parts[0] scores { 语文: int(parts[2]), 数学: int(parts[3]), 英语: int(parts[4]), } students[sid] {name: name, scores: scores} except FileNotFoundError: print(没有找到存档文件直接进入录入模式) return studentstry...except FileNotFoundError是专门针对文件不存在这个预期情况的处理。它不是用来掩盖错误的而是因为文件不存在在业务上完全合理——第一次运行当然没有历史文件程序应该温柔地提示而不是粗暴地崩溃。next(f)用来跳过表头那一行这个技巧很多初学者没见过。文件对象本质上是一个迭代器next(f)相当于手动取第一行然后扔掉后面的循环自然就从第二行开始了。还有一个防御性判断if len(parts) 4: continue防止文件被手改坏了读到残缺行导致程序崩掉。5. 改了这么多份作业这几个错误出现得最频繁5.1 边遍历边删字典RuntimeError 的现场先说每年必现的一个报错。很多学生想把不及格的学生从字典里删掉于是一口气写了这段代码for sid in students: if student_total(students[sid]) 180: del students[sid] # RuntimeError: dictionary changed size during iteration然后当场收获一个RuntimeError: dictionary changed size during iteration。报错信息其实说得很明白你在遍历字典的时候把字典大小改了。Python的字典迭代器会在内部记录当前的键数量你一边遍历一边删它会认为自己看到的数据结构被外部篡改了立刻罢工。正确做法是先把要删的学号收集到一个列表里遍历结束后再统一删除drop_ids [ sid for sid, info in students.items() if student_total(info) 180 ] for sid in drop_ids: del students[sid]这个错误本身不难修但我每次都拿出来讲是因为它背后有一个更重要的思维方式不要在执行遍历修改的一瞬间同时动手。把筛选和修改拆成两个阶段代码会清晰很多也更不容易出逻辑错误。这个原则不止字典适用列表里删元素、文件里删行都是同一个思路。5.2 排序搞错key按姓名排还是按总分排排序是这道题的核心需求出错率也极高。第一个常见错误是忘了reverseTrue把降序写成了升序。第二个是key写错了对象。比如有学生这么写ranked sorted(students.items(), keylambda item: item[0])item[0]是学号于是他按学号排了序和总分没半毛钱关系。这种错法特别隐蔽因为程序不报错运行结果也有模有样地排好了序只有仔细看才发现顺序根本不对。还有一类错误是把key写成lambda s: s[total]但你的学生信息里压根没有total这个字段total是你现算出来的。这时候要么先在每个学生的字典里添加total字段要么在lambda里现算。我推荐后者不污染原始数据。另外如果两个人的总分相同你希望他们再按学号排可以写ranked sorted( students.items(), keylambda item: (sum(item[1][scores].values()), -int(item[1][sid])), reverseTrue )给 sorted 传一个元组作key它会先比第一个元素再比第二个元素。这里把学号取负配合reverseTrue就实现了总分相同则学号小者在前的次要排序规则。这种写法在排序题里非常实用建议记一下。5.3 字符串和数字混着比input() 挖的坑第三个高频错误纯属阴沟里翻船。input()返回的永远是字符串这个知识点课件里都会提但一到实际代码里就忘。比较成绩的时候有人直接拿原始输入来比score input(请输入成绩) if score 60: print(及格) else: print(不及格)这样写语法没错60 60也确实返回True。但问题出在字符串比较是按字典序一个字符一个字符比的字符串9会大于80因为9的编码比8大哪怕9在数值上远小于80。一旦出现两位数和一位数混杂的情况判断就完全失灵了。解决方法就是在录入阶段就完成类型转换try: score int(input(请输入成绩)) except ValueError: print(成绩必须是整数)我在这里想强调的其实不是转换本身而是数据类型意识。写代码的时候要时刻清楚这个变量现在是字符串还是数字尤其从input、从文件里读出来的数据默认都是字符串需要参与计算的时候就当场转换不要拖到后面拖久了你自己都忘了它是什么类型。这也是为什么我平时调试时喜欢在可疑位置打一行print(type(var))确认类型比猜靠谱得多。5.4 输出格式地狱教师机上的严格比对第四个问题不太起眼但杀伤力巨大输出格式。很多作业是老师用自动比对脚本检查的逐字符对比程序的输出和标准答案。你内容算得全对但多了一个空格、少了一个冒号、小数点位数不对结果就是0分。比如平均分保留一位小数应该用f{avg:.1f}而不是str(avg)。str(88.0)和f{88.0:.1f}都输出88.0看似一样但str(83.66666666666667)输出一长串小数而f{83.66666666666667:.1f}输出83.7直接不一样了。我建议写完程序后刻意做一次格式自检把输出结果复制到记事本里和题目示例的输出逐字对比一遍。同时注意Windows下换行符的问题如果你用print()输出的内容被重定向到文件再读取比对时CRLF和LF的差异可能会导致每一行都被判成不同。这个属于进阶问题遇到再说但格式要和示例一模一样这个意识一定要有。5.5 排错链路从看懂报错到定位问题最后说一个通用方法论程序报错时该怎么排错。很多新手看到红色报错就慌然后开始在代码里瞎猜着改运气好改对了运气不好越改越乱。我的排错步骤永远是固定的看报错信息的最后一行那是错误类型和具体信息比如KeyError: total告诉你字典里没有total这个键。先搞清楚发生了什么再想为什么发生。看Traceback里的文件行号从下往上读最后一个代码框通常就是出错的那一行。先看那一行大概率能直接发现问题。如果看不出问题就在出错位置前面加print()把涉及的变量一个一个打出来。打印内容要带上变量名和当前值比如print(debug sid , sid, type , type(sid))。这一步能立刻告诉你数据类型对不对、值是不是预期的。修完之后把临时print删掉重新跑一遍确认没有引入新问题。这个流程听着简单但能坚持这么做的人不多。大多数人倾向于盯着代码看而不是让代码告诉你它看到了什么。Python的报错信息已经写得很友善了学会利用Traceback的信息量排错时间能缩短一半以上。6. 交完作业之后我建议你顺手养成的几个习惯6.1 命名和注释写给一周后的自己作业交上去分数下来这件事就算翻篇了我觉得不是。第五次作业最大的价值是你终于写出了第一个超过一百行的程序这时候开始养成几个习惯后面的路会顺很多。第一个习惯是命名。a、b、c、list1、dict2这种名字写的时候很爽过三天你再回来看完全不知道它是什么。我见过最夸张的一份作业变量名从a排到z排完了开始用aa、ab。算法再简单光读懂这些变量名就要花十分钟。我建议变量名至少能说明含义students、scores、course_names这种就行不要求多高级有意义就比没有强。第二个习惯是注释写为什么而不是是什么。scores[sid] ... # 把成绩存进字典这种注释就是废话代码本身已经说明它在存成绩了。真正有价值的注释是那种解释原因的比如if sid in students: continue # 学号主键去重防止同一个人被录入两次这种注释能帮助未来的你理解当初的设计意图。6.2 边界条件测试空数据、重复学号、超范围分数第二个习惯是测边界情况。很多同学的测试方法是跑一遍有结果就交。但程序真正的bug往往藏在边界里如果录入列表是空的程序会不会崩学号重复了程序有没有友好提示成绩输入了负数、输入了300分程序有没有拦截文件里某一行被人为改坏了读取时会不会崩如果只有一个学生班级平均分、最高分最低分的逻辑还成立吗这些情况你自己不测等老师用测试用例打你的代码那就晚了。我每次写完程序都会专门拿空数据和极端数据各跑一遍。空数据能暴露大部分没有考虑到数据不存在的逻辑缺陷极端数据能暴露类型和范围校验的缺失。这个习惯花的时间不多但能救你很多次。6.3 多版本留档一次改废了还能回退第三个习惯是关于文件管理的。做作业时千万别在同一个文件上反复大刀阔斧地改。今天加个功能明天改个算法改到一半发现思路错了想回到昨天那个能跑的版本结果发现昨天的版本被覆盖了只能坐在电脑前重新敲一遍那个滋味太难受了。我现在写任何有一定规模的小程序都会做版本留档最简单的做法就是按时间或者功能点存副本score_stat_v1.py基础录入和计算score_stat_v2.py加上排序score_stat_v3.py加上文件读写每个版本之间的改动一目了然出问题还能随时回退。如果你已经开始用版本控制工具那更好把这些练习项目都放进去commit信息写成完成录入模块修复排序bug回头翻起来就是一份自己的成长记录。这次作业的代码量放在整个编程学习曲线里根本不值一提但它标记着你第一次独立完成需求→设计→编码→测试→交付的完整闭环。把这个闭环走顺了后面的每一份作业、每一个项目都是在这条路上越走越远而已。 SEO 优化官网定制响应式建站教育培训建站