最近帮我带的一位 Python 新人刚好写完基础阶段的第五次作业,我顺手把这次作业涉及的知识点、踩坑过程和完整代码都整理了一遍。如果你也正在学 Python 基础,或者刚开始带新人练手,这份内容可以直接拿来当参考。
这次的第五次作业,题目量不算大,但很有代表性:它把前面学过的函数、字符串、列表、字典、循环和异常处理全部串到了一起,还顺带加了一道文件读写的扩展题。换句话说,前四次作业是“学会某一个知识点”,第五次开始就是“综合运用”,这两者的难度差一截。我会在下面把每一道题的思路、代码、易错点都拆开讲,最后再汇总一份常见报错排查表,方便你照着自查。
这次作业的完整目标可以概括成三句话:能写带返回值的函数,能熟练操作字典和列表,能读懂并处理常见运行时报错。接下来直接进入正题。
1. 作业整体设计与思路拆解
1.1 为什么第五次作业是分水岭
很多教程会把函数安排在很靠后的章节,但我带人入门时,会刻意早点引入函数。原因是 Python 这门语言真正的生产力,不在你写了多少行顺序代码,而在你能不能把重复逻辑抽象成函数,能不能用容器类型把数据组织好。第五次作业的定位,就是让初学者从“照着例子敲”过渡到“自己设计逻辑”。
这次作业一共五道题,设计上分成了三个梯度:
- 基础巩固题:回文串判断、水仙花数,考察函数定义、字符串操作、循环和列表推导式。
- 综合运用题:单词频次统计、简易计算器,考察字典、函数对象、异常处理。
- 扩展挑战题:把统计结果写入文件,考察 with 语句、文件路径和编码;人狗对战小游戏,考察随机数、循环控制和函数合作。
这样安排的逻辑是:先保证基本语法不出错,再练习容器类型之间的配合,最后落到“程序能跟外部环境交互”这个真实需求上。文件读写那一题我特别建议认真做,因为很多初学者学了半年 Python,还是只会用 print 输出结果,一旦要保存数据就卡住,这是很明显的短板。
1.2 作业范围与前置知识要求
在动笔之前,我建议你先检查自己是不是已经掌握下面这些内容。如果哪一项还模糊,做作业时大概率会卡壳:
- 函数定义与返回值:def 开头,return 返回结果,不能把 print 当 return 用。
- 字符串常用操作:lower()、split()、strip()、切片、isalnum()。
- 列表与字典的增删改查:列表推导式、字典的 get 方法、遍历 items()。
- 循环控制:for 循环遍历容器,while 循环处理“不确定次数”的场景。
- 异常处理基本结构:try / except / else / finally 的简单使用。
这里有一个通用原则可以记住:练习题的意义不是“把题目做对”,而是“你能不能用已学的知识,把一道没见过的题拆解成熟悉的小步骤”。如果你做作业时发现自己每一步都认识,但组合不起来,那说明前面几个知识点的应用还不够熟,可以回头把列表推导式和字典的 get 方法再练两遍。
2. 核心知识点解析与避坑要点
2.1 函数:别在函数里 print 结果,要 return
初学函数时最常见的毛病,是把 print 写在函数内部,然后发现调用函数时看不到返回值,或者想把结果再拿去跟其他数据比较时无从下手。我把原则强调得非常简单:函数是“计算单元”,不是“显示单元”。
举个例子,判断回文串这一题,正确做法是函数返回布尔值:
def is_palindrome(s): s = ''.join(ch for ch in s.lower() if ch.isalnum()) return s == s[::-1] # 调用方来决定怎么用这个结果 print(is_palindrome("A man, a plan, a canal: Panama")) # True if is_palindrome("hello"): print("是回文") else: print("不是回文")这样设计的好处是,同一个函数既能用来直接打印,也能放进 if 条件里判断,还能放进列表推导式批量检测多个字符串。函数一旦返回了具体值,调用方就拥有了“再加工”的自由,这才是函数真正的价值。
注意:如果你把 print 写在函数里,调试时会发现结果虽然能显示,但函数返回的是 None。后面写项目时,这种情况会导致一堆奇怪的逻辑错误,排查起来很头疼。
再补充一个作用域问题。很多新人在函数内部直接写count = count + 1,如果 count 是在函数外定义的全局变量,运行时会报 UnboundLocalError。原因是 Python 认为你在函数内给 count 赋了新值,它就成了局部变量,于是不再读取外部的全局变量。解决办法是用参数把值传进去、用返回值带出来,而不是依赖全局变量。
2.2 字典与列表:一个记频率,一个存结果
第五次作业里,单词频次统计是绝对的核心题。这道题表面上考字符串处理,实际考的是字典的“计数”用法。几乎所有跟“统计”“次数”“频率”相关的需求,到最后都是用字典解决的,比如统计用户行为、统计订单状态、统计日志错误等等。
掌握字典计数的关键,是先搞懂dict.get(key, default)这个方法:
freq = {} for word in words: freq[word] = freq.get(word, 0) + 1这行代码的意思是:如果 word 已经在字典 freq 里,就取出当前次数加 1;如果不在,就按初始值 0 加 1。这样写既简洁又安全,不会因为第一次遇到某个键而触发 KeyError。
列表推导式是另一条主线。水仙花数那题如果不用推导式,得写三四行循环加 append;用推导式一行就能出结果。我建议基础比较薄弱的同学,第一次可以先写标准 for 循环,跑通之后再改写推导式,两步走既能理解流程,又能掌握语法。
2.3 异常处理:程序崩溃不是最可怕的,可怕的是你不知道怎么读报错
第五次作业的简易计算器,核心考点是处理用户输入可能引发的异常。完整的计算器可以这样写:
def add(a, b): return a + b def subtract(a, b): return a - b def multiply(a, b): return a * b def divide(a, b): return a / b operators = { '+': add, '-': subtract, '*': multiply, '/': divide, } def main(): while True: user_input = input("请输入算式(例如 3 + 5,输入 q 退出):") if user_input.strip().lower() == 'q': break parts = user_input.split() if len(parts) != 3: print("格式不正确,必须用空格分隔,例如:3 + 5") continue a_str, op, b_str = parts try: a = float(a_str) b = float(b_str) result = operators[op](a, b) except ZeroDivisionError: print("除数不能为 0,请重新输入") except KeyError: print("不支持该运算符,支持 + - * /") except ValueError: print("请输入数字") else: print(f"结果:{result}") if __name__ == '__main__': main()这里有几处设计值得展开讲:
- 用字典
operators把运算符字符串映射到函数对象,这是 Python 里“函数作为一等公民”的典型用法,比写一长串 if/elif 清晰得多。 - split() 默认按空格切分,所以要求用户输入时数之间必须带空格,这是故意的,目的是简化解析逻辑,先保证核心运算正确。
- try 块里不要塞太多无关代码,只把可能出错的运算、输入转换放进 try,正常输出放到 else,看起来更规整。
- 在 main() 里用 while True + break 实现“反复计算直到用户退出”,这比只跑一次更贴近实际工具的感觉。
实操心得:我见过很多新人写计算器,第一版能跑通加法就把代码停下不继续了,等到被要求加上“连续计算”和“错误处理”时,才发现代码结构一团乱。最好的做法是先把框架搭出来——主循环、函数映射、异常分支——然后一步步补功能。框架对了,后面加需求只是往里填函数的事。
3. 作业题目详解与完整代码实现
3.1 回文串判断:从题目语义到函数设计
题目要求:写一个函数,判断给定字符串是否是回文,回文指正着读和倒着读都一样,且忽略大小写、空格和标点符号。
这道题的坑不在算法,而在“清理字符串”。英文里常见写法是只保留字母和数字,然后统一转小写:
def is_palindrome(s): cleaned = [] for ch in s.lower(): if ch.isalnum(): cleaned.append(ch) return cleaned == cleaned[::-1]如果换成列表推导式,就是:
def is_palindrome(s): cleaned = [ch for ch in s.lower() if ch.isalnum()] return cleaned == cleaned[::-1]这里我把 cleaned 定义为列表而不是字符串,因为 list 直接支持[::-1]反转比较,写起来更直观。如果你想看字符串版本,也没问题:
def is_palindrome(s): s = ''.join(ch for ch in s.lower() if ch.isalnum()) return s == s[::-1]两种写法都可以,但你要能解释每一步在干什么:先转小写,再过滤非字母数字,再反转比较。
注意:isalnum() 对中文也是返回 True,所以这个函数对“上海自来水来自海上”同样有效。不过中文分词不如英文直接,练习阶段用英文串就好。
3.2 水仙花数:用列表推导式冲掉循环行数
题目要求:输出所有三位数中的水仙花数,即一个三位数各位数字的立方和等于它本身。经典答案是 153、370、371、407。
非推导式写法:
result = [] for num in range(100, 1000): digits = str(num) total = 0 for d in digits: total += int(d) ** 3 if total == num: result.append(num) print(result)推导式写法:
result = [num for num in range(100, 1000) if sum(int(d) ** 3 for d in str(num)) == num] print(result)推导式看着炫,但逻辑其实没变:遍历三位数,把每一位转 int 后求立方再求和,判断是否等于原数。我建议你在动手前,先自己在纸上写出sum(int(d) ** 3 for d in str(num))的求值过程,确认理解无误,再用它。
细节提示:范围一定要写 range(100, 1000),因为三位数是从 100 到 999。很多人写 range(1000) 也能跑,但会把 0、1 等个位数也算进来,虽然数字上不影响水仙花数结果,逻辑上却不够严谨。
3.3 单词频次统计:字典 + 字符串清理的黄金组合
题目要求:给一段英文文本,统计每个单词出现的次数,并输出频次最高的 5 个词。
先看完整代码:
def clean_text(text): # 把标点符号替换成空格,再转小写 for ch in '.,!?;:"()[]': text = text.replace(ch, ' ') return text.lower() def word_frequency(text): text = clean_text(text) words = text.split() freq = {} for word in words: if word: freq[word] = freq.get(word, 0) + 1 return freq def top_n(freq_dict, n=5): sorted_items = sorted(freq_dict.items(), key=lambda item: item[1], reverse=True) return sorted_items[:n] if __name__ == '__main__': sample = """ Python is powerful. Python is easy to learn. Python is popular, and Python is fun! """ freq = word_frequency(sample) print(freq) print("Top 5:") for word, count in top_n(freq, 5): print(f"{word}: {count}")这里的关键步骤有三个:
- 清理文本:把常见标点符号替换成空格,再统一转小写。
- 切分:split() 是根据空白字符切分的,所以前面的清理步骤必须把标点变成空格,否则
"fun!"会被当成一个词。 - 计数:用
freq.get(word, 0) + 1实现“有则加一,无则初始化”。
输出排序用了sorted(freq_dict.items(), key=lambda item: item[1], reverse=True),这一行解释一下:items() 返回键值对元组,key 指定按第二个元素(次数)排序,reverse=True 表示降序。
实操心得:很多初学者在这里会写
sorted(freq_dict),结果只对 key 排序,输出的全是单词而不是次数。这个问题我在别人代码里见过无数次,关键是要意识到 sorted 默认遍历字典的键,比如要遍历键值对,必须用 items()。
3.4 简易计算器:从“能算”到“不怕出错”
这个题的能力目标不是写出四则运算,而是让程序在用户给错输入时不崩溃。我在上面 2.3 里给了完整代码,这里再补充两个常见优化方向:
- 支持连续运算:比如用户输入
2 + 3 * 4时,能不能按运算符优先级计算?这需要用到“中缀表达式求值”的思路,基础阶段可以先不实现,但可以留作扩展。 - 支持更多运算符:比如
%取余、**幂运算。扩展起来很简单,只需要在 operators 字典里加一对映射就行。
我建议你完成基础版之后,至少给自己加一个“指数运算”,体会一下“加功能=加字典项”的爽快感。这也是函数映射设计的好处。
3.5 扩展题一:把结果保存到文件
把单词频次结果写入文件,是练习文件读写的好机会。完整代码:
def save_frequency(freq_dict, filepath): with open(filepath, 'w', encoding='utf-8') as f: for word, count in sorted(freq_dict.items(), key=lambda item: item[1], reverse=True): f.write(f"{word}: {count}\n") save_frequency(freq, 'word_frequency.txt')几个关键点:
- 必须显式指定
encoding='utf-8',否则在中文 Windows 环境下,open 默认编码可能是 gbk,写中文就会报错。 - 使用
with语句管理文件,Python 会在代码块结束后自动关闭文件,哪怕中途异常也不会泄露句柄。 - 写入时把排序放在写入循环里,这样保存下来的文件就是按频次降序排列的,方便直接阅读。
实操心得:如果只想快速看结果,用 print 输出到屏幕没问题;但只要数据量一多、需要保存中间结果,就必须用文件。文件读写这个能力做项目时基本绕不开,早晚要练。
3.6 扩展题二:人狗大作战小游戏
这个扩展题来自“用函数模拟回合制战斗”的思路,代码非常简单,但能很好地练习“多个函数配合 + while 循环条件 + random 随机数”这几个知识点:
import random def attack(attacker, defender): damage = random.randint(5, 15) defender['hp'] -= damage print(f"{attacker['name']} 攻击 {defender['name']},造成 {damage} 点伤害") return defender['hp'] <= 0 def main(): player = {'name': '你', 'hp': 100} dog = {'name': '大黄狗', 'hp': 80} round_count = 0 while player['hp'] > 0 and dog['hp'] > 0: round_count += 1 print(f"\n--- 第 {round_count} 回合 ---") input("按回车键攻击...") if attack(player, dog): print("你赢了,大黄狗倒下了。") break if attack(dog, player): print("你输了,已经被大黄狗击败。") break print(f"当前状态:你的血量 {player['hp']},狗的剩余血量 {dog['hp']}") if __name__ == '__main__': main()这段代码里,我把人和狗都表示成字典:name 是名字,hp 是生命值。攻击函数接收两个字典,修改防守方的 hp,然后返回“对方是否已经倒下”。这样主循环就可以用 while player['hp'] > 0 and dog['hp'] > 0 控制战斗继续,一旦某一边 hp 归零,循环自然结束。
如果你想让游戏更有趣,可以扩展出暴击、躲避、技能冷却这些机制,每次扩展本质上都是往字典里加字段、往战斗函数里加逻辑。这个方向非常适合练习函数拆分,也贴近游戏开发的基础套路。
4. 常见错误与排查技巧实录
4.1 最容易踩的五个运行时报错
下面是我整理的基础阶段高频报错,按出现频率从高到低排列:
| 报错信息 | 出现原因 | 快速解决方案 |
|---|---|---|
NameError: name 'xxx' is not defined | 变量名拼写错误,或函数内使用了未定义变量 | 检查是否少字母、大小写是否一致;检查变量是否在作用域内先赋值 |
IndexError: string index out of range | 访问字符串/列表时下标越界 | 检查索引是否超过 len() 范围;注意字符串最后一位索引是 len-1 |
KeyError: 'xxx' | 字典里没有这个键,直接用了dict['key'] | 改用dict.get('key', 默认值),或者先判断 if key in dict |
UnboundLocalError: local variable 'x' referenced before assignment | 函数内变量与外部全局变量同名,又给该变量赋值了 | 不要依赖全局变量,用参数传入、返回值带出 |
ZeroDivisionError: division by zero | 用户输入除数为 0,或者循环里出现了除以 0 | 在除法前判断是否为 0,或用 try/except 捕获 |
这些报错信息本身并不难懂,但新手一看到红字就慌。我的建议是:先把最后一行报错复制到搜索引擎,看明白是什么类型,再回头看具体是哪一行触发的。别从头读一遍大片 Stack Trace,那样效率很低。
提示:读报错信息时,优先看两个位置——最后一行(错误类型)和报错信息里带
File "xxx.py", line N的那一行(出错位置)。这两条信息就足够定位大部分问题了。
4.2 排查步骤:五步定位法
我教新人调代码时,反复强调一套固定流程,遇到任何报错都按这个步骤走:
- 读最后一行报错,确认错误类型(比如 KeyError 还是 TypeError)。
- 找到报错信息里指出的文件行号,打开源码看那一行。
- 在那一行前后各加 print,把变量值打出来,确认实际数据和预期是否一致。
- 思考这行代码在什么数据条件下会报错,反向构造“最小复现案例”。
- 修改代码,跑一次确认,再顺手补充一次异常情况的测试。
举个例子,你写了freq[word] += 1,但报 KeyError。按流程走:第 3 步打印 word,发现第一次遇到新词时字典里根本没有这个键,所以直接加会失败。改成freq[word] = freq.get(word, 0) + 1就能解决。整个过程不到一分钟,但无头苍蝇式地乱试可能折腾半小时。
4.3 语法正确但结果不对的隐藏 Bug
比报错更隐蔽的,是“程序能跑但结果不对”。这里列出我在批改第五次作业时常看到的三类问题:
- 字符串没有清理干净,导致
"Python,"和"Python"被当成两个词。解决方案是在切分前把标点统一替换成空格。 - 函数内使用了全局变量,导致多次调用结果互相污染。解决方案是把所有数据都通过参数传进函数,返回值交给调用方处理。
- 把 return 写在循环体内部,导致只执行一次就退出。这里需要确认 return 的缩进层级;如果想返回整个循环的累积结果,return 必须放在循环外面。
排查这类问题的方法,依然是打印中间变量。如果你发现words里带有Python,,就立刻明白标点没处理干净。如果是函数重复调用结果不对,你就把函数内变量的当前值打出来,看看是否被上一次调用影响。
4.4 环境与工具层面的常见问题
这次作业本身不依赖第三方库,但如果你的运行环境还没配好,也会卡住。下面几个问题我见得多:
- 在终端输入
python --version提示“不是内部或外部命令”,说明 Python 没有加入环境变量,需要去安装时勾选“Add Python to PATH”,或者手动配置环境变量。 - 使用 VS Code 时,右下角提示“没有选择解释器”,运行代码报“Please select a valid Python interpreter”。解决方法是 Ctrl+Shift+P(Mac 是 Cmd+Shift+P),输入
Python: Select Interpreter,选择你装的 Python 版本。 - 文件路径里有中文或空格,导致
open('word_frequency.txt')找不到文件。解决方案是用相对路径,确保当前工作目录就是代码所在目录,或者打印os.getcwd()看当前路径。
实操心得:很多初学者遇到环境问题会误以为是自己代码写错了,白白浪费大量时间。我的建议是,只要报错信息里包含“No module named”“interpreter”“pipeline”,先不要改代码,优先排查环境。代码报错往往在“不可名状”的语法位置,而环境报错通常很直接地说缺什么。
收尾的一点个人经验
带过几轮新人之后,我发现第五次作业这个节点特别有意思。前四次作业大家都能按时交,从第五次开始,就会出现明显的分层:一部分人开始理解“函数 + 数据结构”的组合威力,另一部分人还在靠记忆样例代码硬拼。
我个人的建议是,这次作业别急着把所有题目都一次写完。你可以先独立完成前两题,然后对照答案检查思路;第三题开始尝试自己设计数据结构和函数拆解,哪怕写得丑也行。写完以后,故意把输入换几种极端情况,比如空字符串、全标点文本、除数为 0,看看程序会不会崩。会崩不要紧,正好用上上面那套排查流程。
最后再分享一个小技巧:这次作业里每一道题,你都可以在末尾加一个“运行一下”的注释块,说明输入什么、期望输出什么。别小看这个习惯,它能逼你把手里的代码变成可验证的小工具,远比写完就跑一遍默认输入要实用得多。等到你做真实项目、需要维护旧代码时,就会发现这个习惯有多值钱。