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参数指定排序依据。很多学员第一次接触key=lambda 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, key=lambda s: sum(s["scores"].values()), reverse=True)lambda表达式是第五次作业里大家第一次感觉到"Python还能这样写"的地方。如果觉得lambda太绕,也可以定义一个具名函数:
def get_total(student): return sum(student["scores"].values()) students_sorted = sorted(students, key=get_total, reverse=True)两种写法都行,但从可读性和工程习惯来说,我更推荐后者——给操作一个名字,程序就多了一层自我解释。等到用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, filename="score_ranking.txt"): with open(filename, "w", encoding="utf-8") as f: f.write("排名\t姓名\t学号\t总分\n") for rank, student in enumerate(students_sorted, start=1): total, _ = compute_total_and_average(student) f.write(f"{rank}\t{student['name']}\t{student['sid']}\t{total}\n")使用with open(...) as f是Python推荐的上下文管理器写法,它能保证无论程序是否中途抛出异常,文件都会被正确关闭。这个细节我要求学生必须用,因为它是"看起来无害但没做会埋雷"的典型。很多人在交互式环境里反复打开文件不关闭,起初没感觉,到了一个长任务里文件被占用、内容没落盘,才追悔莫及。
关于编码:我让学员把encoding="utf-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 = f"2025{i+1: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,就用assert:
def 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, filename="score_ranking.csv"): with open(filename, "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerow(["排名", "姓名", "学号", "总分"]) for rank, student in enumerate(students_sorted, start=1): 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的人看,请他用大白话复述每段代码的意图。
第三关:扩展性和鲁棒性
试着连续加三个新需求,比如增加"物理"课程、增加"按姓名模糊搜索"、增加"输出每门课程的及格率"。如果加一个小需求就牵一发而动全身,说明代码结构还不够稳固。如果改起来很顺畅,说明之前的函数划分和数据结构选对了。这一关最能体现"作业题目背后的设计功力",也是从"会写"走向"会设计"的分水岭。
我每年带完一批零基础学员后,都会回头审视这份第五次作业的设计。从最初的单一成绩计算题,到后来加入文件读写、异常处理、代码结构评分、自我验证扩展,它逐渐变成了一个能同时考察语法内化、逻辑拆解、工程习惯的综合性项目。一个看似普通的"第五次作业",实际上是学习者身份转变的节点:从跟着教程临摹的学生,变成能独立设计、实现、验证一个小系统的初级开发者。对这个转变的敏感和强调,才是作业背后最有价值的东西。