Python第五次作业复盘:函数、文件读写与异常处理实战
发布时间:2026/9/9 22:51:42 锦皓数字建站

当初拿到《python第五次作业》这个题目时我其实没怎么当回事——前面四次都糊弄过去了这次能有多难结果写完三道题提交运行通过的那一刻我才发现前面四次的代码和这次比起来确实有点“玩具”了。这次作业的覆盖面很典型函数封装、文件读写、异常处理、基本算法建模几乎把Python基础阶段最重要的几块全串起来了。无论你是正在学Python的大学生还是自学编程准备转行做数据分析、爬虫的入门者这篇复盘应该都能帮你少走不少弯路。作业本身长这样第一题是经典的“李白打酒”考的是递推思路和反向建模第二题是“人狗大作战”一个简单的回合制对战小游戏考的是函数拆解和状态控制第三题要求把前两题的结果写入文本文件并且要处理文件写入过程中可能出现的异常。听起来不复杂但真正动手写的时候坑还挺多。下面我按拿到题目后的实际思考顺序把每一步拆开讲清楚。1. 作业整体设计与思路拆解1.1 这次作业到底在考什么很多人拿到题目第一反应是“这三道题有什么联系”我一开始也是这样觉得李白打酒是一道数学题人狗大作战是一个小游戏写文件又是另一回事完全是三个方向。等我把代码写完回头看才意识到老师这么设计其实是按照一条清晰的训练链路在走。第一题“李白打酒”表面上考的是数学建模实际上考的是如何把一个能用笔算出来的问题翻译成循环和变量变化。它逼着你去思考状态是什么每一步状态怎么变终止条件是什么这三个问题一搞清楚代码自然就写出来了。第二题“人狗大作战”考的是函数封装和状态流转。人和狗各有血量、攻击力、防御力每回合互相攻击直到一方血量归零。如果你把所有逻辑都堆在main函数里代码会乱成一团但如果拆成“创建角色”、“计算伤害”、“执行回合”三个小函数整个流程就会变得非常清晰。第三题“文件输出”则是补上了数据持久化这一课。前面四次的作业代码跑完结果只在控制台里闪一下关掉窗口就没了。这次要求把结果写进文件意味着你要面对编码、异常、文件路径这些真实工程里绕不开的问题。很多自学者写到函数和循环就停了文件读写这块总是跳过结果一接触真实项目就露馅。这一题算是把短板补上了。三道题合起来其实就是一个微型项目的完整流程分析问题 → 拆分功能 → 实现逻辑 → 保存结果。方向很明确就是让你从“会写代码”过渡到“会组织代码”。1.2 拿到题目后正确的心流路径我见过太多同学包括当初的我拿到题目第一件事就是打开编辑器开始敲代码结果敲到一半发现逻辑自己都没想清楚于是来回删改越改越乱。正确做法恰恰相反先别碰电脑拿张纸把流程用自然语言写清楚。拿“李白打酒”举例。题目说的是李白街上走提壶去买酒。遇店加一倍见花喝一斗。三遇店和花喝光壶中酒。试问壶中原有多少酒把这句话翻译成人话就是他出门时壶里有x斗酒遇到一家店酒量翻倍遇到一朵花喝掉一斗。整个过程遇到了三次店、三次花最后壶里的酒正好喝光了问x是多少。这就是一个正向流程初始值x → 遇店乘2 → 遇花减1 → 遇店乘2 → 遇花减1 → 遇店乘2 → 遇花减1 → 最终为0。如果从前往后推你得解方程但如果你从最终状态倒着推就变成了纯算术问题最后一次喝完是0倒推一步遇花前应该是011再倒推一步遇店前应该是1÷20.5。每一步的操作都明确了代码逻辑自然就清晰了。这就是“把问题想清楚再动手”的价值。人狗大作战也是同理。先想清楚一回合要做什么人攻击狗 → 判断狗死没死 → 狗攻击人 → 判断人死没死 → 进入下一回合。把这个流程写下来再开始动手你会发现代码只是把这段流程翻译成Python语法仅此而已。1.3 为什么要用“函数”而不是“一个大脚本”第二题和第三题其实都在逼你使用函数而且不是“因为作业要求所以必须用”而是事实确实如此。我可以用一个生活类比解释这件事你做饭把所有工序压在一张菜谱里理论上也能做出一顿饭但如果你想换个口味就得重新写一张菜谱如果你把“切菜”、“调味”、“焖煮”拆成独立步骤那换个菜可能只需要换其中一两步就行。函数就是这个“独立步骤”。它有三个实打实的好处第一是复用写好的函数可以反复调用不同题目之间还能互相借用第二是可读性别人包括三天后的你自己看代码时一眼就知道每个模块在干什么第三是可测试性单独验证一个函数的输入输出是否正确比在一个几百行的脚本里找bug容易得多。所以这次作业虽然简单但它是培养模块化思维的起点。后面你想做爬虫、数据分析、量化交易策略本质都是把一个复杂任务拆成多个小函数再拼起来。这次不好好掌握后面会非常吃力。2. 核心题解与实操要点2.1 第一题李白打酒——把数学问题翻译成程序逻辑这题看着像奥数题实际上核心是反向推导。前面我说了倒着走比正着推简单但具体到代码实现还有几个细节要处理。我给的实现思路是这样的把六次操作倒过来遇到“花”就加一斗遇到“店”就除以二遍历完这六个操作就得到初始酒量。def compute_initial_wine(): wine 0 # 正向顺序店、花、店、花、店、花 # 逆向操作遇到花回退 1遇到店回退 /2 reverse_actions [flower, store, flower, store, flower, store] for action in reverse_actions: if action flower: wine 1 else: wine / 2 return wine if __name__ __main__: ans compute_initial_wine() print(f李白壶中原有 {ans:.3f} 斗酒)运行结果应该是0.875也就是七分之八斗。这里有个值得注意的点题目和现实世界有个微妙差异。现实中你不可能从半斗酒开始做“遇店翻倍”这样的操作但数学题允许小数存在古代“斗”下面还有更小的容量单位所以0.875斗是合理的。这个细节考察的就是你能不能从具体场景中抽象出数学模型。还有一个坑我必须提醒题目里“三遇店和花”这个描述有歧义。是“店、花、店、花、店、花”交替出现还是“店店店花花花”这种连续出现不同假设会得到完全不同的答案。我在作业里明确写了自己的假设是“交替出现”并在注释里标了出来。这种“把模糊条件显式化”的习惯在真实项目中非常重要否则需求理解不一致代码写出来也是白写。如果你想把这道题做成“输入顺序、输出结果”的通用版本只需要把reverse_actions作为参数传进去代码的适用范围就立刻扩大。这就是函数参数化的意义。2.2 第二题人狗大作战——用函数封装模拟对战流程人狗大作战看起来比李白打酒复杂但其实它的核心架构就是一个死循环循环里是双方轮流攻击。我建议用字典来存角色属性而不是直接用一堆散落的变量因为字典可以让你在一个地方管理角色的所有状态代码会清爽很多。如果你已经学过类当然也可以写成class但从目前的作业进度来看用字典函数就够了不增加认知负担。角色属性我设计成这样player {name: 陈同学, hp: 100, attack: 18, defense: 3} dog {name: 恶犬, hp: 80, attack: 14, defense: 2}然后写一个伤害计算函数import random def calc_damage(attacker, defender): base attacker[attack] random.randint(-3, 3) damage max(0, base - defender[defense]) return damage这里的random.randint(-3, 3)是模拟攻击力的随机浮动max(0, ...)是做一个兜底保护防止伤害变成负数。为什么要做这个保护因为如果防御力高于攻击力波动后的值可能出现负伤害那不就成了“给敌人加血”这在实际游戏里当然不行。对战的完整循环可以这样写def run_battle(): player {name: 陈同学, hp: 100, attack: 18, defense: 3} dog {name: 恶犬, hp: 80, attack: 14, defense: 2} round_no 1 log_lines [] while player[hp] 0 and dog[hp] 0: damage calc_damage(player, dog) dog[hp] - damage log_lines.append(f第{round_no}回合{player[name]}对{dog[name]}造成{damage}点伤害{dog[name]}剩余血量{dog[hp]}) if dog[hp] 0: log_lines.append(你赢了) break damage calc_damage(dog, player) player[hp] - damage log_lines.append(f第{round_no}回合{dog[name]}对{player[name]}造成{damage}点伤害{player[name]}剩余血量{player[hp]}) if player[hp] 0: log_lines.append(你输了) break round_no 1 return \n.join(log_lines)这里有几个关键点。第一round_no在第一回合后没有立刻加一是因为我把它放在本回合所有操作完成之后才更新这个设计保证了每条日志里的回合编号是一致的。第二两个死亡判定放在每次攻击之后可以避免不必要的攻击动作。第三用log_lines列表收集日志而不是直接print是因为第三题要求把结果写入文件把日志先收集到列表里后续直接拼接成字符串写入文件即可一步到位。如果你想让代码更好验证可以加一句random.seed(42)这样每次运行的随机数序列都是固定的结果可复现调试起来很方便。我自己写作业时每次跑结果都不一样还以为逻辑写错了加上随机种子才发现只是随机波动差点白白排查半天。2.3 第三题文件版输出——让结果落盘而不是只显示在屏幕前两道题的结果在控制台能看到但作业要求把它们写进一个txt文件。这一步看似简单实际上是把“程序内部”和“外部世界”打通的关键一步也是最容易出问题的环节。我的实现思路是写一个专门负责文件写入的小函数def write_text(file_path, content): try: with open(file_path, w, encodingutf-8) as f: f.write(content) print(f结果已写入 {file_path}) except PermissionError: print(没有权限写入文件请检查文件是否被其他程序占用) except FileNotFoundError: print(目录不存在请检查路径) except Exception as e: print(f写入失败{e})这里必须强调几个细节。第一encodingutf-8一定要写否则在Windows下默认可能是GBK编码之后用VSCode打开中文就会变成乱码。第二with语句不需要手动关文件出了这个代码块文件会自动关闭这是一个很省心的上下文管理器。第三w是覆盖写模式每次运行都会清空原文件再写入如果你希望追加内容应该用a模式。异常处理部分看起来是几句简单的print但它的价值在于不让程序因为一个小错误就崩溃。比如文件被Word或Excel占用了Windows会抛PermissionError如果没做处理程序直接中断有了 try-except至少用户知道发生了什么而不是看到一个红彤彤的报错堆栈。完整调用可以放在主函数里from li_bai import compute_initial_wine from battle import run_battle from io_helper import write_text if __name__ __main__: result_lines [] result_lines.append( 第一题李白打酒 ) result_lines.append(f李白壶中原有 {compute_initial_wine():.3f} 斗酒) result_lines.append() result_lines.append( 第二题人狗大作战 ) result_lines.append(run_battle()) write_text(result.txt, \n.join(result_lines))这里的if __name__ __main__:是一个固定套路它的意思是只有当你直接运行这个文件时下面的代码才会执行如果这个文件被其他文件导入下面这些“立刻执行”的代码就不会跑。这个机制在工程上非常重要因为别人导入你的模块时一般只想要里面定义的函数而不是希望它自动跑一遍流程。很多新手没写这个写出来的代码被import之后就开始运行结果莫名其妙。3. 完整作业实现方案可直接抄作业3.1 项目目录结构与运行环境先说运行环境。这次作业只需要Python 3.8以上版本不需要安装任何第三方库。很多人一看到“人狗大作战”就想找pygame之类的游戏库完全没必要控制台版本就是题目要求。我在做之前还纠结要不要装random库后来发现random是标准库Python自带的不用pip install任何东西。推荐目录结构如下homework5/ ├── main.py # 主入口负责调度 ├── li_bai.py # 第一题李白打酒 ├── battle.py # 第二题人狗大作战 ├── io_helper.py # 第三题文件读写工具 └── result.txt # 运行后生成的结果文件按模块拆文件的目的是让你养成“一个文件只干一件事”的习惯。等你以后做爬虫项目会分spider.py、parser.py、storage.py思路和这个一模一样。如果你现在还是一股脑把所有代码塞进一个文件三个月后你一定会后悔的。3.2 完整代码与逐段说明下面我把每个文件的完整代码贴出来并解释关键行。整体代码量不大抄下来运行一遍比只看文字理解要快得多。li_bai.pydef compute_initial_wine(): 反向推导李白出门前的初始酒量。 wine 0 # 正向顺序店、花、店、花、店、花 # 反向推回去 # 遇花前应该多1斗遇店前应该少一半 reverse_actions [flower, store, flower, store, flower, store] for action in reverse_actions: if action flower: wine 1 else: wine / 2 return wine if __name__ __main__: ans compute_initial_wine() print(f李白壶中原有 {ans:.3f} 斗酒)这段代码的核心是reverse_actions这个列表。它的顺序和正向相反所以遍历它时我们实际上是在逆推。wine从0开始逆推回初始状态逻辑上相当于从“最终喝光”倒着还原每一次操作。这里如果你写成“正向模拟会怎样”可以用一个变量存初始值遍历正向操作最后输出初始值是否能让终态为0但那本质上变成了穷举验证没有反向推导直观。battle.pyimport random def create_character(name, hp, attack, defense): 创建一个角色字典。 return {name: name, hp: hp, attack: attack, defense: defense} def calc_damage(attacker, defender): 计算一次攻击造成的伤害保证不为负。 base attacker[attack] random.randint(-3, 3) damage max(0, base - defender[defense]) return damage def run_battle(): 执行人狗对战返回完整日志文本。 random.seed(42) # 固定随机种子方便调试复现 player create_character(陈同学, 100, 18, 3) dog create_character(恶犬, 80, 14, 2) round_no 1 log_lines [] while player[hp] 0 and dog[hp] 0: damage calc_damage(player, dog) dog[hp] - damage log_lines.append(f第{round_no}回合{player[name]}对{dog[name]}造成{damage}点伤害{dog[name]}剩余血量{dog[hp]}) if dog[hp] 0: log_lines.append(你赢了) break damage calc_damage(dog, player) player[hp] - damage log_lines.append(f第{round_no}回合{dog[name]}对{player[name]}造成{damage}点伤害{player[name]}剩余血量{player[hp]}) if player[hp] 0: log_lines.append(你输了) break round_no 1 return \n.join(log_lines) if __name__ __main__: print(run_battle())你可能会问random.seed(42)有什么意义它让随机数在每次运行时都产生同样的序列这意味着同一段代码跑出来的结果完全一致。调试时特别方便——如果每次结果都不一样你很难判断修改代码后是变好了还是变坏了。你可以在提交作业前把这一行注释掉让老师每次运行看到不同的对战过程更有“随机真实感”但调试阶段一定保留它。io_helper.pydef write_text(file_path, content): 写入文本到指定文件处理常见异常。 try: with open(file_path, w, encodingutf-8) as f: f.write(content) print(f结果已写入 {file_path}) except PermissionError: print(没有权限写入文件请检查文件是否被其他程序占用) except FileNotFoundError: print(目录不存在请检查路径) except Exception as e: print(f写入失败{e})这里有个小技巧except Exception as e放在所有具体异常后面作为兜底。如果你把Exception放在最前面它会拦截所有异常后面的具体处理逻辑就永远不会执行。这个顺序问题很多新手踩过坑。main.pyfrom li_bai import compute_initial_wine from battle import run_battle from io_helper import write_text if __name__ __main__: result_lines [] result_lines.append( 第一题李白打酒 ) result_lines.append(f李白壶中原有 {compute_initial_wine():.3f} 斗酒) result_lines.append() result_lines.append( 第二题人狗大作战 ) result_lines.append(run_battle()) write_text(result.txt, \n.join(result_lines))把流程串起来的正是main.py里的这几行。“导入函数 → 收集结果 → 写入文件”三个步骤清晰可见哪怕你没看过前面的代码读到这个文件也知道整个项目在干什么。这就是组织代码的力量。3.3 如何自测你的代码正不正确交作业之前一定要自己测一遍。我总结了几个非常实用的自测点李白打酒的结果已知答案是0.875如果程序输出0.875说明反向推导逻辑正确。如果输出别的值先检查你的reverse_actions顺序是不是写反了。人狗大作战的胜负如果你把狗的初始血量改成1并且固定随机种子第一回合人大概率就能赢。改回原值再跑结果可能出现波动。用随机种子固定之后每次结果应该完全一致如果两次不一致说明循环里存在被改动的外部状态比如某个变量被意外修改了。文件内容跑完 main.py 后打开 result.txt确认第一题和第二题的内容都存在并且第一题的答案写在第二题之前。如果你连续运行两次result.txt的内容应该被覆盖更新而不是越写越长因为你用的是“w”模式。异常测试把 result.txt 改成只读再次运行 main.py程序应该输出“没有权限写入文件”而不是直接崩溃。再把 result.txt 删掉正常跑一次确认文件会被自动创建。这两步验证的是文件模块是否真的健壮。这些测试在开发环境里都跑一遍只需要几分钟但能避免你在上课演示的时候当众翻车。我的经验是提交前花十分钟自测比事后跟老师解释为什么结果不对要划算得多。4. 常见问题与排查技巧实录4.1 高频报错速查表这次作业里我身边同学遇到的报错几乎集中在几个固定类型。我把它们整理成一张表你可以对照着排查。报错信息出现原因解决方法NameError: name player is not defined变量名拼写不一致或者变量在另一个函数里定义没传进来检查函数参数有没有漏传代码里所有player的拼写是否一致IndentationError: unexpected indent缩进不一致Tab和空格混用在VSCode右下角把缩进方式统一为“空格”并设置缩进为4个空格TypeError: unsupported operand type(s)用 拼接字符串时一边是字符串另一边是数字用f-stringf剩余血量{hp}别用剩余血量 hpTypeError: float object cannot be interpreted as an integer循环次数或索引用了浮点数李白打酒的除法会产生float如果你用它做range参数就会报错检查是不是误用了除法结果FileNotFoundError: [Errno 2] No such file or directory文件路径或目录不存在用绝对路径调试或者先确认当前工作目录使用os.getcwd()查看UnicodeDecodeError/UnicodeEncodeError编码不一致读写文件时没有指定utf-8open时统一加encodingutf-8RecursionError: maximum recursion depth exceeded递归没有终止条件或者写成了无限递归检查递归函数的 base case确保每次调用都向终止条件靠近我印象最深的是IndentationError。有个同学在函数里混用了Tab和空格VSCode里看着是好的一运行就报缩进错误。这个报错非常隐蔽因为它不是逻辑问题而是“看不见的字符”问题。建议你在VSCode里开启“显示空格”功能一眼就能看出哪里混用了。4.2 一个困扰了我一整晚的问题乱码与编码我自己遇到最大的坑就是中文乱码。写文件时没有指定encodingutf-8在Windows下默认会按GBK编码写入。结果我用VSCode打开 result.txt中文全部变成“鈥斺€”这种奇怪的符号。检查了半天才发现是编码问题。解决方式有两个你在作业里任选一个都行。第一最直接的方法就是统一在open()里加encodingutf-8包括读文件和写文件第二如果你在VSCode里打开文件时发现乱码可以直接点击VSCode右下角的编码按钮选择“通过编码重新打开”选UTF-8文件就能正常显示了。这个坑看起来小但对作业影响很大。老师打开你的result.txt如果看到满屏乱码哪怕代码逻辑全对印象分也会大打折扣。另外还要提醒一点如果你在Windows的记事本里写代码保存过文件可能带着BOM头一个不可见的前缀这在Python脚本读取时可能引发Unexpected UTF-8 BOM错误。解决办法是用VSCode重新保存在右下角编码选择UTF-8不带BOM我因为这个踩过不只一次。4.3 代码风格与命名老师不会明说但会扣分的点作业交上去之后我才意识到代码风格这件事有多重要。我最初给变量命名用的是a,b,c比如a 100代表玩家血量b 80代表狗血量。当时觉得无所谓反正自己能看懂但过了一天回头看完全想不起来a是血量还是攻击力。后来我改成了player_hp、dog_attack这种命名代码瞬间好读很多。命名这件事我建议直接养成好习惯变量名用小写加下划线snake_case函数名也要小写加下划线比如calc_damage不要用CalcDamage或者calcDamage。名字要能表达用途尽量写得像一个简短的句子不要用单字母。还有print的输出格式要统一。不要第一行用print(血量 str(hp))第二行用print(f剩余血量{hp})两种风格混着来。建议全程用f-string它是Python 3.6以后最推荐的格式化方式写起来直观也避免了类型转换的手误。我再看自己第五次作业的代码最大的进步是开始关注“别人看到这份代码是什么感受”。这其实是一个很重要的转变因为真实的项目是团队协作的你的代码要被人review被后来的维护者读。早一点建立这种意识后面进入真实项目的时候会轻松非常多。最后再分享一个我保留到现在的小技巧提交作业前把项目目录整体复制一份把 result.txt 删掉从一个干净的目录重新运行一遍。这样做是为了确保你的代码不依赖电脑里的历史文件、缓存或者其他“隐形状态”。很多作业在你自己电脑上能跑换台机器就各种报错多半是因为依赖了没写进代码的环境状态。每次提交前先跑一次“干净环境测试”这个习惯我一直留到了现在从写作业到做爬虫项目、写数据分析脚本从来没有因为环境问题被当场拦住过。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。