Python零基础第五次作业:从语法到函数、文件与工程实践的转折点
发布时间:2026/9/8 14:09:50来源:尧图网络
1. 为什么偏偏是第五次作业一次从语法到工程的转折点带过几轮零基础学Python的学员之后我发现一个很有意思的规律第一次作业交上来全班基本都能按时完成第二次、第三次开始有人掉队到第五次作业往往会出现明显的分水岭。前四周大家还在熟悉print、if、for、list这些零件到了第五次如果作业设计得当就该逼着大家把零件组装成一台能运转的小机器了。我一直跟学员强调学Python不是背语法手册而是训练一种拆解问题→设计流程→用代码表达的思维方式。所以第五次作业我通常不会只让大家写一个孤立的算法题而是会设计一个带有真实场景、需要自己拆分步骤、自己定义函数、处理异常的小项目。这次作业的核心目标有三个一是检验前四周的基础语法是否真正内化二是强迫大家从照着示例敲代码切换到自己设计代码结构三是让同学们第一次体会到程序出错不可怕可怕的是不知道怎么找错。这篇文章不是照搬某一次具体的作业题而是把我设计第五次作业的完整思路、踩过的坑、以及批改作业时反复看到的高频问题整理出来。不管你是正在带学生的老师还是自学Python想找个阶段性检验的练习或者刚入职场的开发新人想补充点工程习惯都能从这里找到可以直接照着做的东西。先说结论第五次作业的最佳形态不是一道更难的算法题而是一个小但完整的真实任务。目标不是考倒谁而是让每个认真做完的人都能拍着胸脯说一句我现在能独立写一个能跑、能出错、还能自己抓错的程序了。2. 作业设计思路让学员在已知与未知的边界上跳跃2.1 前四次作业的台阶是怎么铺的在设计第五次作业之前我必须先想清楚前四周学生到底掌握了什么以及哪些地方只是假装掌握了。第一次作业通常是环境搭建加跑通Hello World我见过太多同学在这一步就卡住了。问题往往不在代码本身而是Python环境没有配好、文件路径含中文、或者用的是某些很老的教程推荐的Python 2.7环境。所以第一次作业我要求提交的不是代码本身而是运行截图加一段回答解释一下__name__变量的作用。这个任务很轻但能逼着大家去看官方文档而不是只复制别人的代码。第二次作业开始接触数据类型、运算符和字符串操作。我会布置一些对着真实数据操作的练习比如从一段包含时间、金额、用户名的日志文本里提取信息然后做简单计算。这个阶段大家迷糊的点集中在字符串和数字的转换、input()读进来的是字符串、格式化输出的各种花式玩法。第三次作业进入流程控制和列表操作我会让学员写一个循环加分支的小程序比如猜数字游戏。因为涉及while循环和break、continue我开始能看出哪些人对循环条件在什么时候结束缺乏直觉。猜数字游戏虽然简单但要写得聪明比如限制输入次数、处理非数字输入已经能拉开差距了。第四次作业上函数。我会给出若干个计算任务比如批量计算一组数据里的均值、方差、最大最小值要求至少封装成三个函数并写一个main()来串联。到这里学生的代码开始出现结构化雏形但多数人的函数定义还是把以前写成一段的代码切分成几块不涉及参数传递和返回值的深层理解。第四周结束的时候我做了个小测验结果非常诚实大约只有三分之一的人能清楚说出局部变量和全局变量的区别。这就是第五次作业必须解决的断层。2.2 作业题目让数据自己说话第五次作业的核心题目我一般设计成一个**成绩统计与查找系统**。这个题目没有市面上那些花哨的爬虫量化噱头但它天然具备几个教学上的优点需求非常生活化不需要任何领域知识就能理解。涉及的数据结构嵌套列表或字典列表是后续做任何真实项目的地基。天然适合拆分为多个函数检验函数设计的合理性。能自然引出文件读写、异常处理、边界条件这些工程里绕不开的问题。具体需求长这样我会根据学员情况微调某班级有N个学生N 5每个学生的信息包括姓名、学号、三门课程的成绩语文、数学、英语。请你从键盘输入这N个学生的信息然后实现以下功能统计并输出每个学生的总分和平均分按总分从高到低输出所有学生的排名输入一个学号查找并输出该学生的所有信息如果找不到给出友好提示统计每门课程的最高分、最低分和平均分将排名结果写入一个文本文件文件名为score_ranking.txt。先别急着觉得这也太简单了。我这几年的经验是越是看似简单、边界明确的题目越能暴露出各种你以为你会了的盲区。先请大家在脑海中大致规划一下代码结构再往下看我对每个需求点的拆解。2.3 为什么选这个题五个需求点背后的教学意图需求1和需求2看起来都是计算但实现思路完全不同。统计总分平均分是遍历一遍就能出结果的线性任务而排序则要求理解Python的sort、sorted的用法特别是如何通过key参数指定排序依据。很多学员第一次接触keylambda x: x[2]时会非常懵所以我额外强调了两种写法一种是用operator.itemgetter另一种是自定义一个返回总分的小函数。这里设计排序的需求目的不是让大家背API而是理解排序的对象是列表里的每个完整学生记录而不是单独的成绩。需求3的查找功能强烈推荐用字典来组织数据以学号为键学生信息为值。这样查找复杂度是O(1)代码也直观。用列表遍历也能做但等数据量大了之后体验完全不同。我故意不强制指定用哪种数据结构就是希望有人能自己栽个跟头再爬出来——只有写过一个用列表遍历5000条记录的程序才会真心体会到字典的好。需求4的统计其实和第1个需求有重叠但视角不同。前者是按学生维度聚合后者是按科目维度聚合。按行处理和按列处理看起来只差一个循环方向但对初学者来说遍历嵌套数据结构时外层循环和内层循环到底谁是行、谁是列是非常容易混乱的。需求5的文件写入是第五次作业教学价值的隐藏大头。Python的open、write、with语法入门教程里都写了但实际一跑就报错的人非常非常多原因从路径不存在、编码不对中文字符写入ANSI环境乱码、到忘记关闭文件、用print模式而不是w模式导致多次运行时数据永远停留在第一次。每届学生的错误清单都出奇地相似。3. 从零到能跑的完整实现逐块拆解作业的核心代码3.1 定义数据结构和录入部分我建议学员用列表套字典的结构来存储全部学生数据这样比二维列表更清晰比定义Student类更轻量——毕竟这是第五次作业类还没学到但如果用户已经学过类也可以用类来实现这是加分项。students [] def input_students(): n int(input(请输入学生人数: )) for i in range(n): print(f正在录入第 {i 1} 个学生的信息) name input(姓名: ) sid input(学号: ) chinese float(input(语文成绩: )) math float(input(数学成绩: )) english float(input(英语成绩: )) student { name: name, sid: sid, scores: { chinese: chinese, math: math, english: english } } students.append(student) return students这里容易踩的第一个坑是录入循环结束后很多学员会在函数外重新写一遍for i in range(n)来录入结果变量n作用域混乱。我反复强调过一个原则函数的输入尽量通过参数传入输出通过return返回不要用全局变量在函数间默默传递数据。上述代码把录入逻辑封装成input_students()函数然后通过return students把数据交给调用方。另一个很实际的经验是录入成绩时建议统一转换为float而不是int。虽然成绩一般是整数但统计平均分时会涉及除法如果录入时是int后面计算平均分时偶尔会踩到整数除法还是浮点除法的坑。与其到时候到处加float()不如录入时就统一类型。3.2 统计与排序的正确姿势录入完成后第一个任务是计算总分和平均分。我会提示学员不要重复写三遍students[i][scores][chinese] ...这种冗长代码而是用循环def compute_total_and_average(student): scores student[scores] total sum(scores.values()) average total / len(scores) return total, averagePython内置的sum()和len()在这里特别好用不过要注意scores.values()返回的是一个视图对象不能直接索引即不能写scores.values()[0]。新手最常见的错误就是想用下标取第一门成绩但其实遍历或者用sum()才是更符合惯例的做法。接下来是排序。排序的关键是用key参数def sort_by_total(students): return sorted(students, keylambda s: sum(s[scores].values()), reverseTrue)lambda表达式是第五次作业里大家第一次感觉到Python还能这样写的地方。如果觉得lambda太绕也可以定义一个具名函数def get_total(student): return sum(student[scores].values()) students_sorted sorted(students, keyget_total, reverseTrue)两种写法都行但从可读性和工程习惯来说我更推荐后者——给操作一个名字程序就多了一层自我解释。等到用pandas做数据分析时你会发现这种先把字段提取逻辑定义成函数再丢给排序/分组/聚合API的思路是相通的。3.3 查找功能和友善失败查找的逻辑不难难在用户输入了一个不存在的学号时怎么办。很多初学者直接写if sid in [s[sid] for s in students]: ... else: print(未找到)这样不是不行但每查一次就要完整遍历一次列表效率上不够优雅。如果用字典存储查找就变成了一次哈希操作。def build_index(students): index {} for student in students: index[student[sid]] student return index def find_student(index, sid): if sid in index: return index[sid] return None我特别想强调这里的友善失败思维程序不能在一遇到异常输入时就崩溃而是应当给出人话级别的提示并让程序继续运行。这是真实项目和课堂作业之间的重大区别。很多学员第一次写找不到学号时只是简单print一句但打印完程序就结束了更好的设计是让用户能循环查询直到输入q退出while True: sid input(请输入要查询的学号输入 q 退出: ) if sid.lower() q: break result find_student(index, sid) if result is None: print(f抱歉学号 {sid} 不存在请确认后重试。) else: print(result)3.4 文件写入与编码问题的传统艺能文件写入环节是最能引爆问题的。我的示例代码def write_ranking(students_sorted, filenamescore_ranking.txt): with open(filename, w, encodingutf-8) as f: f.write(排名\t姓名\t学号\t总分\n) for rank, student in enumerate(students_sorted, start1): total, _ compute_total_and_average(student) f.write(f{rank}\t{student[name]}\t{student[sid]}\t{total}\n)使用with open(...) as f是Python推荐的上下文管理器写法它能保证无论程序是否中途抛出异常文件都会被正确关闭。这个细节我要求学生必须用因为它是看起来无害但没做会埋雷的典型。很多人在交互式环境里反复打开文件不关闭起初没感觉到了一个长任务里文件被占用、内容没落盘才追悔莫及。关于编码我让学员把encodingutf-8显式写上这是对跨平台可移植性的一种保护。Windows笔记本默认GBK编码如果只用open(filename, w)写入的中文虽然本机能读但把文件拷给Mac或Linux用户就乱码。这种问题排查起来非常恼人所以一开始就养成显式指定编码的习惯能省掉无数后患。4. 历届学员踩坑实录五大高频问题与排查思路4.1 问题一TypeError: dict_keys object is not subscriptable这个错误几乎每届都会有人踩。原因是在compute_total_and_average里学员写了类似keys scores.keys(); first_score scores[keys[0]]的代码。Python 3中dict.keys()返回的视图对象不支持下标访问。解决办法是改写成迭代或使用sum(scores.values())。我把这个错误单列出来是因为它代表了一类很泛的问题很多初学者默认只要集合就有下标Python里很多可迭代对象并不支持随机访问。这不仅出现在字典视图上后面学习生成器、map、filter返回值时也会遇到同样的困惑。学会看报错信息里的关键词not subscriptable基本就能知道自己试图对一个不可下标的类型用了[]。4.2 问题二UnboundLocalError: local variable total referenced before assignment这个报错非常经典。当学员写了类似这样的代码时就会触发total 0 def add_score(score): total score return total原因是函数内对total进行了赋值操作Python就认为它是一个局部变量而局部变量在第一次赋值前被读取于是报错。解决办法是使用global total声明或者更推荐的做法不要把状态挂在全局变量上而是让函数接收当前值并返回新值def add_score(total, score): return total score我在作业讲评里特别强调这个模式因为它直接指向函数式编程的核心理念尽可能让函数无副作用不修改外部状态输入决定输出。这个理念后面写多线程、写测试、写大型项目时都至关重要。4.3 问题三运行时输入一个个敲太麻烦有没有更好的调试方式录入5个学生还能忍受录入50个学生时每跑一次程序要敲上百次键盘异常影响调试效率。我给学员的解决思路是写一个自动生成模拟数据的子程序用随机数填充这样测试排序和查找逻辑时完全不用手动录入。import random def generate_mock_students(n): names_pool [张伟, 王芳, 李娜, 刘洋, 陈晨, 杨柳, 赵磊, 孙悦, 周杰, 吴昊] students [] for i in range(n): name random.choice(names_pool) sid f2025{i1:03d} scores { chinese: random.randint(60, 100), math: random.randint(60, 100), english: random.randint(60, 100), } students.append({name: name, sid: sid, scores: scores}) return students这个技巧看似偷懒其实是软件工程里非常核心的实践把人工测试和逻辑测试解耦。哪怕你现在只是写作业我也建议养成程序功能可以独立于输入方式的思维——把数据的获取方式和业务处理逻辑分开后续维护、扩展、自动化测试都会受益无穷。我还鼓励学员试试把generate_mock_students作为默认输入方式当用户按下某个选项时才进入手动录入模式这样代码反而更像一个完整的交互系统。4.4 问题四全班排名出来了但程序一结束数据就没了这个问题和文件写入直接相关。有些同学运行程序后看到了屏幕上的排名但没意识到这些结果只存在内存里关掉程序就消失了。他们往往把输出到屏幕和保存到文件混为一谈。我讲了两个可以立刻验证的类比内存像一块白板写完随时可以擦掉硬盘像一个抽屉东西放进去关上抽屉才能下次打开还在。如果排名结果只print到屏幕上下次运行程序时根本找不到上次的结果。只有写入文件数据才真正落地持久化了。这也是为什么需求5一定要留文件写入这种打扫战场的步骤——它逼着学生从程序运行完就算了过渡到程序要产出可以留存的成果。4.5 问题五代码整体能跑但看不下去批改作业时我见过不少代码能正确运行但结构非常原始的版本。典型特征包括所有逻辑从头到尾写在一个main()或者干脆裸写在全局没有任何函数封装。变量名是a、b、c、lst完全没有语义。同一个求总分的逻辑在程序里出现了三遍却不知道抽成函数。没有缩进风格或者缩进、引号混用。这些能跑但看不下去的代码单独看任何一行都没有大错但放到一个500行的真实项目里就是灾难。第五次作业的评分标准里我给了代码结构分的权重。学习编程必须学会用未来会有别人读我的代码的标准来要求自己。它不复杂但需要刻意练习而这次作业正好是练习的起点。5. 代码审查课怎么把能跑的程序打磨成能给别人看的程序5.1 一份能跑但幼稚的样本为了让学员直观理解结构差和结构好的区别我每年都会在讲评课上展示一段反例代码。它的运行结果完全正确但代码是这样的感觉s [] n int(input()) for i in range(n): name input() sid input() a float(input()) b float(input()) c float(input()) s.append([name, sid, a, b, c]) # ...然后是一长串索引操作s[i][2] s[i][3] s[i][4]这段代码能拿到全部分数点但没有任何一个公司愿意把代码维护交到这样的人手上。尤其是二维列表里的成绩用s[i][2]、s[i][3]这样魔法数字索引来提取一旦后期要加一门物理整个程序的所有下标都要重写极易出错。这个反例的核心问题不是会不会写代码而是有没有用数据结构表达清楚业务。5.2 重构思路从数据访问方式入手我推荐的重构方式是先把二维列表换成字典列表把魔法数字换成有名字的键。这是一次成本很低收益极高的重构因为字典的键名在阅读代码时直接告诉你这是在取语文成绩而不是一个让人摸不着头脑的索引号。然后把读取成绩的操作抽成函数。比如def get_score(student, subject): return student[scores][subject]看起来只省了一点重复但这意味着如果未来成绩结构变了比如成绩变成{语文: 88, ...}的字典只需要改这一个函数而不是在几十个地方修改索引。这就是封装的价值把容易变化的细节藏在一个稳定的接口后面。5.3 边界条件和如果用户乱输怎么办讲代码审查时我还会拿出一个专门的时段讲边界条件。很多人写程序时只考虑正常输入这一条路径从不考虑如果用户输入的不是数字怎么办如果用户人数为0怎么办如果分数超过100怎么办。以第3.4节查找为例用户输入学号q退出是没有问题的但假如用户输入Q呢假如用户输入 q 带空格呢如果不对输入做任何清洗和判断程序就会走入歧途或直接异常。更稳妥的做法是先对输入做strip()和lower()处理再判断是否为退出指令最后匹配学号。这里要把握的度是不需要应对所有可能输入的极端情形但至少要能优雅处理最明显的边界。这既是一种工程素养也是对真实用户行为的敬畏。很多同学一开始觉得这很琐碎——哪有用户会输入带空格的q啊——直到自己写了小程序给别人试用才亲眼看到别人是如何不按套路出牌的。5.4 单元测试的初体验第五次作业讲评时我会顺手演示一个极简单元测试的概念。不用pytest就用assertdef test_compute_total_and_average(): student { name: 测试, sid: T001, scores: {chinese: 80, math: 90, english: 70} } total, average compute_total_and_average(student) assert total 240, f总分应为240实际为{total} assert abs(average - 80.0) 1e-9, f平均分应为80实际为{average}这个示例给了学员一个很重要的暗示程序本身是可以被检验的而不只是靠肉眼观察输出。写自动化测试的习惯如果能在入门阶段就种下后面学任何框架都会轻松很多。我不要求大家在每周作业里都写assert但对函数要设计成可测试的即输入输出清晰这件事的敏感度希望从这次作业开始建立起来。6. 进阶扩展与真实场景的衔接作业做完之后还能怎么玩6.1 从成绩系统到任意结构化数据管理系统很多学员做完第五次作业会有一个疑问这个成绩统计系统也太简陋了真实项目怎么可能这么简单说得对它当然简单但这个作业背后的模式几乎可以套用到所有对一批结构化记录做增删改查、统计、排序、导出的场景。比如学生成绩系统 → 员工薪酬系统 → 库存管理系统只需要把姓名、学号、语文数学英语成绩换成员工编号、部门、基本工资、绩效奖金把按总分排序换成按总薪酬排序把写入score_ranking.txt换成写入salary_report.csv如果学员学完基础语法后想做个拿得出手的项目我建议就在这个作业基础上不断加需求增加删除学生功能、增加按班级筛选功能、增加修改成绩功能、增加读入CSV文件而不是手动输入功能。每加一个需求就复习了一次数据结构和流程控制的组合用法比漫无目的地去刷一堆互不相关的算法题有效得多。6.2 用CSV和Excel打交道真实工作中更常见的文件格式如果学生感兴趣我会提前展示一下如何把排名结果输出为CSV而不是纯文本import csv def write_ranking_csv(students_sorted, filenamescore_ranking.csv): with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([排名, 姓名, 学号, 总分]) for rank, student in enumerate(students_sorted, start1): total, _ compute_total_and_average(student) writer.writerow([rank, student[name], student[sid], total])utf-8-sig这个小细节值得记一下它会在文件开头写入BOM头让Excel以UTF-8编码打开时不乱码。如果你直接把utf-8的CSV用Excel打开经常会看到中文乱码改成utf-8-sig之后问题就消失了。这种踩过才知道的小技巧比死记API更能让人建立起对编码问题的敏感。6.3 引入pytest让代码进化到可测试的层级如果学员进度快作业做完后我会鼓励他们安装pytest把之前用assert写的小测试改成pytest用例。下面是同一段逻辑的pytest版本import pytest from score_system import compute_total_and_average def test_compute_total_and_average_basic(): student { name: 测试, sid: T001, scores: {chinese: 80, math: 90, english: 70} } total, average compute_total_and_average(student) assert total 240 assert average pytest.approx(80.0)pytest的好处是测试收集、跳过、异常断言等功能都内置了而且输出的测试报告非常人性化。把作业里的纯函数全部抽出来然后为每个函数写一个测试文件这其实就是在经历从脚本开发到项目开发的一个关键认知跃迁。我记得有个学员完成这个扩展后跟我说原来测试不是负担是安全感来源。现在改代码前先跑一遍测试心里特别踏实。这句反馈让我觉得这类进阶引导是值得做的。7. 作业批改手记三个让我印象深刻的代码片段7.1 没有用字典但写出了属于自己的坚持有一份作业数据的存储用的是二维列表学号查找用的是for循环加标志位。从最佳实践的角度看这份代码确实可以用字典优化得更高效但这位同学额外写了一个非常清晰的分步注释并用一个自定义函数把查找过程中找到了就立刻返回、找完没找到再返回None的逻辑讲得明明白白。我在评语里先表扬了注释的清晰程度然后引导如果改用字典查找就从遍历N次变成一次哈希定位你可以试试对比两种写法的运行时间。批改这份作业让我体会到作为老师不应该只按标准答案打分而应该看重学生是否展现了解决问题的独立思考。技法可以慢慢学但思维习惯一旦养成后面进步会非常快。7.2 把输入函数抽象成了一个方法有个学员写得很不错他把录入、校验、构建学生对象等逻辑拆成了多个小函数并单独写了一个validate_score来检查成绩范围。更让我眼前一亮的是他在文档字符串里写了一句如果分数不在0-100之间应该提示用户重新输入而不是直接崩溃。这正是我在课堂上一再强调的友善失败思维。这份作业在评分时给了我很大的信心说明设计良好的作业题目确实能把工程思维的种子埋进初学者脑子里。7.3 全班唯一一个在写完文件后尝试用Pandas读回验证的人有个学员做完排名写入后自己主动安装了pandas并把CSV读回来验证了一下。虽然这个动作在作业要求之外但他这个写完数据后要验证、要用不同工具去检查结果的习惯已经超越了对Python语法本身的掌握。这种自驱力甚至比多会几个函数更重要。每次看到这类超出作业要求的举动我都会在讲评课上当众表扬一句这就是真正做项目的姿态——写完代码不是结束验证结果、确保数据正确才是闭环。8. 给自学者的调整建议没老师判作业怎么自我检验自学的人做这道题少了老师的批改和同辈的对比很容易陷入代码能跑就算完成任务的舒适区。我给自学者的建议是设置三个自我检验关卡。第一关功能完整性跑通需求1到需求5能用正常数据完成所有操作这是底线。检验方法是黑白盒都走一遍白盒指的是自己读代码确认逻辑分支齐全黑盒指的是假装自己完全不懂代码按用户视角把所有交互都点一遍包括输错学号、输入空值、输入小数成绩等。第二关代码可读性问自己几个问题如果一个月后的我回来看这份代码能不能在10分钟内重新理解每个函数在做什么函数命名是不是直接说清楚了动作有没有重复代码可以合并如果答案犹豫就动手重构。如果找不到问题试试把自己的代码拿给另一个学Python的人看请他用大白话复述每段代码的意图。第三关扩展性和鲁棒性试着连续加三个新需求比如增加物理课程、增加按姓名模糊搜索、增加输出每门课程的及格率。如果加一个小需求就牵一发而动全身说明代码结构还不够稳固。如果改起来很顺畅说明之前的函数划分和数据结构选对了。这一关最能体现作业题目背后的设计功力也是从会写走向会设计的分水岭。我每年带完一批零基础学员后都会回头审视这份第五次作业的设计。从最初的单一成绩计算题到后来加入文件读写、异常处理、代码结构评分、自我验证扩展它逐渐变成了一个能同时考察语法内化、逻辑拆解、工程习惯的综合性项目。一个看似普通的第五次作业实际上是学习者身份转变的节点从跟着教程临摹的学生变成能独立设计、实现、验证一个小系统的初级开发者。对这个转变的敏感和强调才是作业背后最有价值的东西。
网站建设高端定制企业官网