资讯动态

Python第二次作业复盘:李白打酒、列表去重与函数边界全解析

发布时间:2026/10/9 6:51:27 来源:尧图企业网站定制
说实话看到老师把“Python第二次作业”发下来的时候我内心是有点膨胀的。第一次作业无非是打印几行字符串、算几个简单表达式第二次再离谱能离谱到哪去结果题目往屏幕上一放我盯着“李白打酒”四个字看了十秒钟——这到底是编程题还是语文题后来我才反应过来第二次作业才是真正把同一批人拉开距离的开始第一次作业考记忆第二次作业考理解。这篇文章是我完成这份作业的完整复盘包括每道题上的第一反应、绕进去的坑、最后怎么爬出来。这次作业具体包含五道题李白打酒的程序实现、列表去重、数组切片相关操作、自定义函数解决素数判断、一个简单的文件读取与处理。听起来都是基础知识点但真正动手之后我发现自己对循环顺序、变量作用域、默认参数这些概念的理解全都在代码报错的一瞬间坍塌了。如果你也刚开始学Python正被第二次作业折磨照着这个思路走会省下不少时间。1. 作业拿到手的时候题目表和我最初的解题计划先把原题亮出来方便对号入座。我们这次作业一共五道难度并不是均匀分布的我当时扫了一遍就大概有了判断题号题目内容考察方向我最初的判断1李白打酒遇店加一倍见花喝一斗三遇店和花喝光壶中酒求原有多少酒循环、递推、逆向思维看着像数学题实际是编程题最容易被绕晕2给定一个包含重复元素的列表要求去重且保持原顺序列表操作、去重算法以为很简单但保持顺序这一点有讲究3实现字符串反转、切片取偶数位、按步长提取子序列切片规则、索引教科书上的题但切片的步长反向很容易出错4自定义函数判断一个整数是否为素数并统计100以内的素数个数函数定义、循环边界函数是重点边界条件容易漏5读取一个txt文件统计里面每个单词出现的次数并输出前5个文件操作、字典、排序综合性最强最容易翻车这里多说一句我建议大家拿到作业不要急着敲代码先花十分钟做两件事第一把每道题的输入和输出写清楚尤其是格式第二按“会做的先做不会做的先想”的顺序排个优先级。我当时把第五题排在最后就是因为文件路径和编码问题在Windows和Mac上表现不一样属于环境相关的坑留到最后处理比较稳妥。我的计划是先做第三题切片因为它最机械能快速进入状态再做第二题去重建立信心接着做第四题函数把自定义函数的语法练熟然后回头啃第一题李白打酒最后用第五题收尾。事实证明这个顺序救了我——如果一上来就死磕李白打酒我可能两个小时都出不来后面几道题全得赶工。2. 李白打酒一道递推题把我绕进去的完整过程2.1 先从正向模拟说起原题是“无事街上走提壶去买酒。遇店加一倍见花喝一斗。三遇店和花喝光壶中酒。试问壶中原有多少酒”这里的关键是理解顺序不是先连续遇见三次店再连续遇见三次花而是“遇店、见花、遇店、见花、遇店、见花”交替发生一共六次操作最后一次是见花。很多同学卡住就是把顺序理解错了。如果顺着这个顺序想设壶中原有酒量为x那么经过一次“遇店加一倍、见花喝一斗”之后壶中酒变成 2x - 1。连续三组操作之后结果是0。展开就是[ ((2 \times (2 \times (2x - 1) - 1)) - 1) 0 ]化简之后是 8x - 7 0所以x 7 / 8 0.875斗。数学上很容易但作业要求的是用程序实现。我最初的思路是用正向模拟加穷举假设初始酒量是某个值模拟三次遇店见花看最后是不是0。于是写了下面这段代码def simulate(wine): for _ in range(3): wine wine * 2 wine wine - 1 return wine # 穷举一位小数 for i in range(100): init i / 100 if abs(simulate(init) - 0) 1e-6: print(初始酒量为, init)运行结果输出0.875逻辑没错。但用穷举小数去碰一个精确值在这个题目里勉强能用换成精度要求高的情况就会出问题。比如我把步长改成0.0001循环次数要变成8750次才能碰出结果这显然不是最优解。2.2 逆推才是标准解法后来我把这题重新理了一遍发现正确的做法是逆推。最后一次见花之后酒是0那么最后一次见花之前酒量是多少见花喝一斗所以喝之前是0 1 1斗。再往前一次操作是遇店遇店加一倍那么遇店之前应该是1 / 2 0.5斗。如此倒着一组一组往前推每一步都是把“加一倍”变回“除以2”把“喝一斗”变回“加一斗”wine 0 for _ in range(3): wine wine 1 # 逆推见花把喝掉的1斗加回来 wine wine / 2 # 逆推遇店把加倍的量还原 print(wine)输出是0.875。这段代码只有三行循环背后却是完全不同的思维模型。正向模拟是在“猜答案”逆推是在“还原过程”。我一开始不敢用逆推是因为总担心循环里的加法和除法顺序会弄反。后来我给自己总结了一个记忆方法逆推的时候循环体里先写加法再写除法顺序刚好和正向过程反过来——正推是先乘法再减法逆推就是先加法再除法。2.3 浮点数的老朋友问题这题还有个小坑就是浮点数精度。如果你直接打印wine得到的是0.875这个数在二进制里能精确表示所以输出正常。但如果原题把3次改成10次逆推出来的结果可能就是一长串小数比如0.4999999999这种。我试了一下把循环次数改成10用浮点数逆推最后打印出来是0.11641532182693481之类的数字拿去跟别人对答案时就会产生“我算错了”的错觉。这时候可以用Python标准库里的Fraction来避免浮点数误差from fractions import Fraction wine Fraction(0, 1) for _ in range(3): wine wine 1 wine wine / 2 print(wine) # 输出 7/8 print(float(wine)) # 0.875作业里不强制要求用Fraction但我建议了解它。第二次作业的意义不在于把答案跑出来而在于让你意识到同样的逻辑用精确运算和浮点运算会有完全不同的结果。这个认知在后续学机器学习、数据计算的时候非常值钱。3. 列表去重和切片Python内置功能够用就别自己造轮子3.1 “筛选一样的”三种去重写法各有代价第二题给了一个包含重复元素的列表要求去重且保持原顺序。我当时第一反应是直接用set因为前面学集合的时候老师强调过set有去重功能nums [3, 1, 4, 1, 5, 9, 2, 6, 5, 3] result list(set(nums)) print(result)输出是[1, 2, 3, 4, 5, 6, 9]顺序完全乱了跟题目要求的“保持原顺序”对着干。这个教训很直接set本身是无序的它适合的是不关心顺序的场景。如果作业只要求去重set一行搞定一旦要求保持原顺序就得换思路。第二种写法是遍历加判断result [] for num in nums: if num not in result: result.append(num) print(result)输出[3, 1, 4, 5, 9, 2, 6]顺序正确。这段代码逻辑很朴素但有个隐患result里元素越多每次判断num not in result需要遍历的时间就越长属于O(n²)复杂度。数据量小无所谓数据量大了会很慢。第三种写法是我后来学到的用dict.fromkeysresult list(dict.fromkeys(nums))dict.fromkeys会把列表元素作为字典的键而字典天然不能有重复键并且Python 3.7之后字典能保持插入顺序。所以这个方法既去重又保序而且底层是哈希表效率远高于第二种。运行结果同样是[3, 1, 4, 5, 9, 2, 6]。我把三种写法对比了一遍发现一个很扎心的事实Python内置的数据结构本身就覆盖了很多常见需求我一开始只会用set是因为我对字典的特性不够熟。这就是典型的知识够用但不全面。3.2 切片不是随便写个中括号就行第三题是切片操作看起来最没有技术含量实际上手最容易错。比如字符串反转很多人会写循环s HelloPython result for ch in s: result ch result print(result)这样能实现反转但Python里一条切片就够了s HelloPython print(s[::-1]) # nohtyPolleH关键在于理解[start:stop:step]三个参数。step为正数表示从左往右取step为负数表示从右往左取。很多人搞混[::-1]的原理其实就是start和stop都省略step设为-1Python会从最后一个元素开始反向取到第一个元素。同理取偶数位是s[::2]取索引2到8之间每隔3个是s[2:9:3]。我写了一个小例子验证s abcdefghij print(s[2:9:3]) # cfi索引2取c索引5取f索引8取i print(s[9:2:-2]) # jhfdb吗不对实际是 jhd等等我在这里就踩坑了。s[9:2:-2]从右往左取索引9是j然后7是h然后5是f然后3是d再往左是索引1但stop是2意味着取到索引2就停下来索引2不包含所以结果是jhd。我当时把结果写成了jhfdb提交前才发现。切片的边界规则就一句话包含start不包含stop。不管是正步长还是负步长都遵守这一条。这题做完我最大的收获是不要用其他语言的思维写Python切片。C或者Java里你会写循环Python里切片是语法层面的能力用好了代码不仅短而且不容易出错。4. 函数定义与变量作用域第二次作业里最隐蔽的丢分点4.1 自定义函数的基本结构第四题是自定义函数判断素数。我一开始写得非常啰嗦def is_prime(n): if n 1: return False for i in range(2, n): if n % i 0: return False return True count 0 for num in range(2, 101): if is_prime(num): count 1 print(count)功能是对的但区间判断用了range(2, n)这意味着对每个n都要从2一直试到n-1。比如判断97是不是素数要试95次。实际上只需要检查到平方根就够了因为如果n有一个大于平方根的因子那必然同时存在一个小于平方根的因子。优化后import math def is_prime(n): if n 2: return False if n 2: return True if n % 2 0: return False for i in range(3, int(math.sqrt(n)) 1, 2): if n % i 0: return False return True这个版本不仅快还处理了2这个特殊偶数。作业批注里老师还提醒素数判断的边界条件最容易漏n1要返回Falsen2要返回Truen是偶数直接排除。我第一版里漏了n2的判断虽然range(2,2)会跳过循环最终返回True看起来没错但逻辑上不够严谨。函数设计要覆盖边界输入这是第二次作业给我上的很重要的一课。4.2 全局变量和局部变量的幽灵问题第五题统计单词频率时我顺手把计数器写在了函数外面然后试图在函数里修改它结果报错count 0 def count_word(word): count count 1 # UnboundLocalError: local variable count referenced before assignment这个报错很多新手都见过函数内部给变量赋值时Python默认把它当成新的局部变量于是左边的count还未定义就被引用了。解决办法有两种一是用global声明count 0 def count_word(word): global count count count 1但更推荐的做法是把计数器放在函数外部让函数通过返回值返回结果保持函数的纯粹性def process_file(filename): word_count {} with open(filename, r, encodingutf-8) as f: for line in f: words line.strip().split() for w in words: word_count[w] word_count.get(w, 0) 1 return word_count这个经历让我明白函数不是一段能改外部变量的代码它应该是一个输入到输出的映射器。第二次作业的批改标准里也明确写了“函数内部尽量不要访问外部变量”这是编程习惯的问题比代码能不能跑更重要。4.3 默认参数和返回值的坑同一天我还试了带默认参数的函数def add_item(item, lst[]): lst.append(item) return lst第一次调用add_item(1)返回[1]第二次调用add_item(2)返回[1, 2]而不是[2]。原因是默认参数lst在函数定义时只创建一次后续调用都共享同一个列表对象。这个知识点在第二次作业里其实有点超纲但既然遇上了就记住可变对象不要做默认参数应该写成lstNone在函数内部再判断。返回值的坑则更隐蔽。我写文件统计时有个分支忘记写return结果函数返回None后面排序的时候直接报TypeError。排查了半天才发现是某个if分支没有返回值。从那以后我养成了一个习惯函数里的每个分支都检查是否有明确的返回值不能让某些路径悄悄返回None。5. 调试与运行环境从安装配置到跑通这堆小题目5.1 环境变量和pip跑通之前最容易被卡住的一步我身边不少同学卡在了环境上。作业发下来那天有人在群里问“python不是内部或外部命令”这就是环境变量没配置好。Windows上安装Python时第一步有个“Add Python to PATH”复选框没勾选的话后续就会很痛苦。解决的路径有两个重装勾选或者手动把Python安装目录和Scripts目录加到系统PATH里。另一个高频问题是pip安装第三方库失败。我统计单词时想用collections里的Counter这属于标准库不需要pip。但当时有人为了更快直接去装numpy结果卡在pip下载慢、版本不匹配、以及“pip 不是内部命令”。其实第二次作业完全用不到numpy我后来也理解了不要因为看到网上说好就提前装作业需要什么再装什么才最省事。让我印象很深的是同学的报错ImportError: numpy.core.multiarray failed to import。他下载了numpy之后又下载了opencv之类的包版本冲突导致numpy起不来。最后卸掉重装指定版本才恢复。这类问题普遍存在我的建议是先跑通作业再说环境问题不要恋战。5.2 用print和注释定位问题我的调试习惯第五题文件统计是我耗时间最长的。我当时的文件读写代码with open(data.txt, r, encodingutf-8) as f: content f.read() print(content)先确认文件能读出来再写统计逻辑。如果直接一股脑写完再运行一旦结果不对根本不知道是读取的问题、切分的问题还是字典统计的问题。我的做法是每写一小步就print中间结果确认无误再继续。比如统计之前先打印split后的单词列表words content.lower().split() print(words[:20])这样能立刻发现标点符号会影响单词统计。比如“hello,”会被当成“hello,”而不是“hello”所以需要清洗标点import re words re.findall(r[a-zA-Z0-9], content.lower())正则表达式对第二次作业来说有点超前但确实解决了实际问题。这也是作业的隐藏价值它逼迫你用一点标准库之外的东西。5.3 运行时报错信息不是让你背的是给你线索的训练自己读报错是第二课。作业做下来常见的报错就几类IndexError是索引越界KeyError是字典键不存在TypeError是类型不匹配UnboundLocalError是局部变量被提前引用。遇到报错不要慌先看最后一行是什么类型再往上找出错行。我花最多时间的是把报错信息复制去搜索结果答案往往不适应自己的代码。后来我总结了顺序先看类型、再看行号、再读代码上下文、最后才考虑搜索。6. 作业提交之后我对Python学习节奏的复盘6.1 这次作业暴露的知识缺口交完作业我重新对照题目列了一张清单发现自己真正的问题集中在三处第一是循环思维。李白打酒这道题正向模拟写得出来但逆推需要把循环里的每一步操作反向理解我在“先加后除”还是“先除后加”之间反复犹豫。这说明我对循环体的迭代过程没有形成肌肉记忆只会照着题目描述顺序写不会倒过来思考。第二是数据结构的特性。列表去重那题我知道set能去重却不知道dict.fromkeys能保持顺序。这说明我对Python内置数据结构的底层特性了解太浅。知识面不是靠背文档而是靠每个真实需求去触发。第三是函数设计。我能写出能运行的函数但写不出考虑边界、没有副作用的函数。这需要刻意练习不是多看几行代码就能会的。6.2 给正在写第二次作业的人几条实操建议基于这次作业的教训我整理了几条能直接用的建议第一先列输入输出示例。每道题在动手前把输入和预期输出写在注释里这比先写代码重要得多。比如李白打酒预期输出就是0.875或者7/8有这个标尺在后面怎么写都不偏。第二复杂逻辑先画流程再写代码。不一定要画正规的流程图哪怕在纸上写出“先读文件、再清洗标点、再统计、再排序”这几步都比盯着编辑器发呆强。文字流程图能有效降低写代码时的认知负担。第三每道题做完之后问自己一个问题这个解法还有没有更Pythonic的写法去重那里我最初只会set和循环最后发现dict.fromkeys更优雅文件统计那里我最初用循环手写计数后来知道了collections.Counter。每次多问一句学习速度就会快一截。6.3 学习节奏比聪明程度重要这次作业让我彻底放下了一个包袱写得慢不是笨是大脑正在建立回路。李白打酒我花了快四十分钟才完全理解逆推过程但理解之后后面所有的循环题都变得顺畅了。第二次作业真正的难度不在于某个知识点有多深而在于它第一次要求你同时运用概念、逻辑和工具。这份作业做下来的过程比我刷十遍网课都有效。最后再分享一个小细节。我把作业里写的每一段错误代码都单独存了一个文件比如error_float.py、error_global.py注释里写清楚当时的报错和解决思路。第二次作业才过去两周我回头看这些文件已经能看到自己当时的可笑和可敬之处。如果你也在学Python建议你也这么做——三个月之后再翻你会感谢现在这个认真踩坑的自己。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑