Python循环练习指南:从for/while边界到嵌套循环实战
循环语句大概是Python学习路上第一道真正的分水岭。前面学变量、学判断、学基本数据类型写出来的代码都是从第一行顺着往下走一眼能看完心里有底到了for和while程序开始自己转圈转几次、什么时候停、循环变量每一步变成什么值全得先在脑子里跑一遍。不少人是这样的状态书上的循环例子逐行看得懂一合上书让自己写就卡在“我该用for还是while”“range里到底写几”这种地方。这不是语法没记住而是缺少一种“在脑子里单步执行”的手感而手感这东西只能靠一道道小练习喂出来。下面这些练习都是我在带人入门和自己回头复习时反复用过的。它们看着简单但没有一道是纯粹的“抄一遍就完事”每一道都埋着一个具体的边界问题range的右开区间、累加变量的初始值、break到底跳出了几层、continue之后计数器有没有被跳过、遍历列表时能不能顺手删元素。题目按难度递进每道都给出代码、推导过程和我实际踩过的坑。适合刚学完循环语法想找题练手的人也适合已经写了半年Python、但某些细节一直靠“试出来”而不是“想明白”的人。1. 循环练习真正要练的是三样东西1.1 写得出、看得懂、改得动是三档不同的能力很多人把“会循环”当成一个开关要么会要么不会。实际带人的经验是这门手艺至少分三档。第一档是写得出给一个明确的需求能照着语法把for i in range(...)搭起来哪怕要多试几次。第二档是看得懂别人写的嵌套循环、带break和continue的循环能一行行在脑子里走完说清楚第3次迭代时各变量是多少。第三档是改得动需求变了比如从“打印1到100”变成“打印1到100里所有能被3整除但不能被5整除的数”能立刻判断该在原有结构上改哪里而不是推倒重来。大部分人卡在第一档到第二档之间原因是练习方式不对只看不写或者只写“标准答案型”的题。标准答案型题目有个特点代码写出来和书上几乎一样脑子全程不需要做判断。真正能提升手感的是带边界的题差一个等号就错、少一次迭代就漏、多一层缩进就崩的那种。做完之后你会被迫去想“为什么是i1不是i”这个“想”的过程才是练习的价值所在。1.2 先练for再练while顺序别搞反我见过不少人一上来就练while理由是“while更自由”。自由恰恰是问题。while把“什么时候停”这个责任完全交给人条件写错就是死循环循环变量忘了推进也是死循环而初学者的调试能力往往还不足以快速定位死循环的原因结果就是对着一个卡住的终端发呆学习热情直接被浇灭。for相反迭代次数和迭代源是绑定在一起的range(1, 10)就是9次不会多也不会少出错的空间被压缩到“range参数写对没有”这一件事上。所以我的建议很明确先用for把“循环体被执行N次”这件事练到不需要思考再回头练while。等你能凭直觉写出for i in range(len(data))并且清楚它为什么等价于遍历索引while的那些坑才有能力接住。提示判断自己是否真的掌握for有个简单的自测方法——不看资料说出range(2, 11, 3)会产出哪几个数。答案是2、5、8。如果你需要默念一遍才能确认说明右开区间这个概念还没变成肌肉记忆。2. 用三道热身题把range的边界感砸实2.1 打印九九乘法表的左下三角range右边界最容易错的地方九九乘法表是个老掉牙的题目但它的价值在于内层循环的右边界是动态的这正好是range最容易出错的地方。要求是打印左下三角也就是第1行只有1列第2行有2列一直到第9行有9列。for i in range(1, 10): for j in range(1, i 1): print(f{j}x{i}{i * j}, end\t) print()关键在于range(1, i 1)这个1。因为range是左闭右开range(1, i)只能取到i-1第i行就少打印一列。初学者在这里的典型错误是写成range(1, i)然后盯着输出看了半天发现每行都比预期少一格却又说不出少在哪。更隐蔽的错误是外层写成range(1, 9)结果整个表少了第9行因为9被右开区间吃掉了。我给这道题加过一个变体把输出改成右上三角也就是第1行9列、第9行1列。这时候内层循环要写成range(i, 10)空格用\t * (i - 1)补齐。同一个range边界从“跟着外层变大”翻转成“跟着外层变小”做完这一正一反右开区间的直觉基本就建立了。2.2 累加与阶乘初始值为什么必须是0和1求1到100的和代码短得不能再短total 0 for n in range(1, 101): total n print(total) # 5050但这里有个值得停下来想一秒的问题total为什么初始化为0因为加法有单位元0任何数加0不变。如果初始化成total 1结果会多1。同样地求阶乘n!的累乘变量必须初始化为1而不是0因为乘法单位元是1初始化为0的话0 * 任何数永远是0循环跑完结果还是0。这个道理说起来像废话但我在实际代码里见过不少次“累加变量没初始化就被”的错误Python会直接抛NameError反而好查真正难查的是语义错了但语法没错的情况比如统计商品总价时把累加变量初始化成了第一个商品的价格循环里又从头加了一遍第一个商品被算了两次。练习时我建议养成一个习惯写累加之前先问自己“如果循环一次都不执行结果应该是多少”这个值就是初始值。这个习惯比记住“0和1”更有用。2.3 反向遍历负数步长和空range的静默失败range的第三个参数可以是负数range(10, 0, -1)会产出10到1共10个数。这道练习的价值在于让你体验“range可以什么都不产出”——比如range(10, 0, 1)起点大于终点但步长为正结果是空的循环体一次都不执行也不会报错。for i in range(10, 0, -1): print(i, end ) # 输出10 9 8 7 6 5 4 3 2 1我习惯在练习里塞一个“倒着打印列表但不修改原列表”的小任务给一个列表从最后一个元素打印到第一个。写法有两种for i in range(len(data) - 1, -1, -1)或者for item in reversed(data)。前者的边界最容易写错终点必须是-1而不是0否则第一个元素打印不到因为右开区间会把它排除掉。这种“差一个”的错误不会报错只会静默地少一行输出只能靠对照预期输出来发现。练的时候一定要养成先写下预期输出再运行的习惯否则你根本不知道自己有没有错。3. while的重点从来不是语法而是谁负责推进循环变量3.1 猜数字循环条件写在哪比怎么写更重要while的结构是“条件为真就继续”但真实需求往往不是“某个变量满足条件”而是“用户还没猜对”。这两种描述对应两种完全不同的写法。import random target random.randint(1, 100) count 0 guess 0 while guess ! target: guess int(input(猜一个1到100的数)) count 1 if guess target: print(大了) elif guess target: print(小了) print(f猜对了总共用了{count}次)这里我把guess初始化成0因为random.randint(1, 100)保证目标值不可能是0循环一定能进得去。但仔细想这个初始化是有讲究的如果目标值可能是0这个条件就永远为假循环直接跳过。while的前置状态必须让条件为真这是很多人第一次写while时想不到的点。更稳妥的写法是while True配合break把“什么时候退出”这件事放在循环体内部判断前置状态就完全不需要操心了。3.2 while True加break看起来偷懒其实是最稳的写法接着上面的例子改写成import random target random.randint(1, 100) count 0 while True: guess int(input(猜一个1到100的数)) count 1 if guess target: print(f猜对了用了{count}次) break print(大了 if guess target else 小了)这么写的逻辑更贴近人的思维先做事做完判断要不要停。而while guess ! target的写法要求你在进循环之前先想清楚初始状态多了一个需要维护的变量。我在实际项目里处理“读文件直到读完”“接收消息直到收到结束标志”这类场景时几乎都用while True加break因为退出条件往往要在循环体中间才能判断出来硬塞到while条件里会让代码变形。注意while True不等于死循环前提是循环体里存在一条能走到break的路径。写完一定要问自己所有分支走一遍有没有哪条路径永远碰不到break输入类型错误、抛出异常这类情况也要考虑int(input())遇到非数字会直接抛异常退出反而是安全的。3.3 用整除代替浮点除到1为止的边界处理这道题我管它叫“数字收敛”给一个正整数偶数就除以2奇数就乘3加1问多少步之后变成1。写起来是这样n 27 steps 0 while n 1: if n % 2 0: n // 2 else: n 3 * n 1 steps 1 print(steps) # 111这里有两个坑。第一是除法必须用//而不是/。用/的话n会变成浮点数n % 2 0的判断虽然还能用但数值一大就会出现精度问题而且n 1在浮点下可能出现1.0000000000000002这种值循环停不下来。第二是循环条件是n 1而不是n ! 1因为如果哪一步意外跳过了1! 1会让循环永远继续而 1至少能保证单调收敛的方向。这个区别在正常数学下看不出但在调试阶段能救命。4. break、continue、else三个被严重低估的关键字4.1 for-else的语义比if-else陌生得多for和while可以带else子句这个else在循环自然结束时执行如果中途被break打断就不执行。判断质数是它最经典的用法def is_prime(n): if n 2: return False for i in range(2, int(n ** 0.5) 1): if n % i 0: return False return True这是用return提前退出的写法。换成for-elsen 97 for i in range(2, int(n ** 0.5) 1): if n % i 0: print(f{n}不是质数) break else: print(f{n}是质数)else块只在“循环把所有i试完都没break”的情况下执行语义正好就是“找不到因子”。但我必须提醒for-else的可读性对不熟悉的人很不友好团队协作时我更倾向于用显式的标志变量或者函数封装。练习时用它来理解语义没问题写进生产代码前先看看同事会不会在review里问你“这个else是挂在哪个if上”。还有一点很多人没注意到如果循环体内有continueelse照样会执行因为continue不是break。而如果循环一次都没执行比如range(2, 2)是空的else也会执行。这两条组合起来用for-else判断质数时n 1这种小于2的情况必须单独拦掉否则会被误判成质数。4.2 continue在while里的陷阱计数器被跳过continue的作用是跳过本次循环剩下的语句直接进入下一次。在for里它很安全因为迭代由range或迭代器接管你不推进它也会推进。但在while里推进循环变量的语句如果放在continue之后就永远不会执行i 0 while i 10: if i % 2 0: continue print(i) i 1这段代码会卡在i 0上死循环因为0 % 2 0成立continue直接跳到下一轮i 1永远轮不到。修法有两个把i 1提到循环体最前面或者用for改写。我更推荐后者for i in range(10): if i % 2 0: continue print(i, end ) # 输出1 3 5 7 9for天然免疫这个问题这也是我前面说“先练for”的原因之一。凡是能用for表达的循环就不要用while硬写这不是风格问题是错误率问题。4.3 嵌套循环里的break只跳一层这个是新手最容易误解的break只跳出当前这一层循环外层的循环不受影响。for i in range(3): for j in range(3): if j 1: break print(i, j) # 输出(0, 0) (1, 0) (2, 0)外层i照样从0跑到2内层每次都在j 1时被掐断。如果需求是“一旦找到就整体退出”需要用标志变量或者干脆写成函数用returndef find_first(matrix, target): for row in matrix: for value in row: if value target: return True return Falsereturn天然穿透所有层这是函数封装最实在的好处之一。我在实际代码里看到过用for-else嵌套来模拟多层跳出的写法能跑但读的人要在脑子里绕三圈不值得。5. 嵌套循环从打印图形到处理二维数据5.1 星号金字塔空格数和星号数的推导过程打印图形是练嵌套循环最直接的方式因为输出对错一眼就能看出来。以5行金字塔为例rows 5 for i in range(1, rows 1): spaces rows - i stars 2 * i - 1 print( * spaces * * stars)关键在于先把两个数量关系推出来而不是试。第i行从1开始的星号数是2i - 1这是奇数序列1、3、5、7、9。空格数要让金字塔居中最后一行空格为0往上每行多一个所以是rows - i。推导清楚之后代码就是直接翻译一行都不用试。如果不想推公式还有一种思路是找“中心列”固定宽度2 * rows - 1每一行的星号分布在中心两侧i - 1的范围内。这种写法更通用画其他形状也套得上。两种思路我都让人练过前一种训练数学直觉后一种训练通用性各有各的用处。5.2 二维列表遍历索引和外层变量什么时候需要对上处理表格数据时双层for几乎是标配。给一个矩阵要求打印每个元素及其坐标matrix [ [1, 2, 3], [4, 5, 6], [7, 8, 9], ] for i, row in enumerate(matrix): for j, value in enumerate(row): print(f({i},{j}){value}, end ) print()这里用enumerate而不是range(len(...))原因是可读性。range(len(data))这种写法在需要同时修改元素时才必要比如matrix[i][j] * 2如果只是读enumerate或者直接for row in matrix更清爽。我见过不少人习惯了for i in range(len(x))写什么都带个索引结果代码里全是data[i]读起来很累。有一个隐性坑如果内层循环要修改元素必须用索引访问for value in row拿到的是元素的副本对不可变对象而言改value不会影响原列表。这一点在练二维数组的时候要专门验证一次否则很容易写出“循环跑完了但数据没变”的代码。5.3 冒泡排序把嵌套循环练成肌肉记忆冒泡排序是嵌套循环的经典练习它的价值不在于排序本身实际项目里都用sorted或.sort()而在于两层循环的边界是相互依赖的data [5, 2, 9, 1, 7, 3] n len(data) for i in range(n - 1): swapped False for j in range(n - 1 - i): if data[j] data[j 1]: data[j], data[j 1] data[j 1], data[j] swapped True if not swapped: break print(data) # [1, 2, 3, 5, 7, 9]内层的上界是n - 1 - i因为每跑完一轮最大的元素就被推到了末尾下一轮不需要再比较它。外层是n - 1轮因为n个元素排好n - 1个最后一个自然就位。swapped标志是提前退出的优化如果某一轮一次交换都没发生说明已经有序直接跳出。我在练这道题时犯过一个错把swapped False写在两层循环之间而不是外层循环内部结果第一轮之后swapped永远是True提前退出逻辑完全失效。这类错误不看代码是发现不了的只能靠在循环里打一行print看每轮的中间状态来定位。这也是我接下来要讲的调试方法。6. 循环里最容易踩的五个坑我基本都踩过6.1 边遍历边删除列表长度在你脚下变化nums [1, 2, 2, 3, 2, 4] for n in nums: if n 2: nums.remove(n) print(nums) # [1, 2, 3, 4]结果里还剩一个2原因是遍历用的是内部索引删掉一个元素之后后面的元素整体前移迭代器跳过了一个。这个坑的隐蔽之处在于它不报错只是结果不对。正确做法有三种列表推导[n for n in nums if n ! 2]、倒序索引删除for i in range(len(nums) - 1, -1, -1)、或者while循环配索引手动控制。列表推导最干净我基本只用这一种。同样的坑在字典上也存在遍历字典的同时增删键会抛RuntimeError: dictionary changed size during iteration这个至少会报错比列表好查。6.2 循环变量在循环外还有值Python的for循环变量在循环结束后仍然存在值是最后一次迭代的值如果循环一次都没执行变量根本不存在。for i in range(5): pass print(i) # 4不报错 for j in range(0): pass print(j) # NameError这个行为和C、Java不一样从那些语言转过来的人容易在这里翻车以为循环结束变量就失效了结果在循环外引用到了脏值。我在代码里从不在循环外使用循环变量即使要用也会换个名字重新赋值避免这种隐式依赖。6.3 浮点数累加的精度偏差total 0 for _ in range(10): total 0.1 print(total) # 0.99999999999999990.1在二进制浮点里不能精确表示累加十次自然会偏。这不是Python的问题所有用IEEE 754浮点的语言都这样。涉及金额时我一定用decimal.Decimal或者干脆以“分”为单位用整数计算。练习阶段知道这个现象就够了真正踩到是在做账单、对账这类功能的时候。6.4 range的参数必须是整数n input(请输入行数) for i in range(n): # TypeError: str object cannot be interpreted as an integer print(* * i)input返回字符串range只接受整数。改成range(int(n))就行。这里顺带一提int()遇到非数字输入会抛ValueError稍微正式一点的练习里应该加try-except或者至少用.isdigit()先判断一下。6.5 缩进层级错一行语义完全变了total 0 for i in range(1, 6): total i print(total)和total 0 for i in range(1, 6): total i print(total)前者每次迭代都打印一次输出1、3、6、10、15后者只打印最终结果15。Python靠缩进划分代码块这类错误在语法上完全合法只能靠看输出发现。我建议一开始就用支持缩进参考线的编辑器能少很多这类低级错误。7. 练习怎么越做越有效三个提高效率的做法7.1 给每道题加一条约束条件光把题做出来收益是有限的。同样一道“求列表最大值”的题加一条约束“不能用内置的max”你就得自己写for加比较再加一条“列表可能是空的”你就得考虑初始值和边界再加一条“列表里可能全是负数”你就得反思初始值能不能设成0。一条约束就是一层新的思考比换十道同难度的题管用。我给自己定过一个规则每道题做完至少加一条约束重做一遍。比如九九乘法表加“只能用一层循环”你就得用字符串拼接配合format冒泡排序加“不能改动原列表”就得先copy一份。这个过程会把原来“背下来”的解法重新拆开理解层次立刻不一样。7.2 循环调试三板斧打印、打印、再打印循环出错时最有效的调试手段永远是在循环体内打印中间变量。不要指望用调试器一步步入初学阶段对调试器不熟反而更慢。我的习惯是三步第一步在循环体第一行打印循环变量的当前值确认迭代次数对不对。第二步在每个if分支里打印走了哪个分支确认条件判断有没有写反。第三步在循环结束后打印最终结果和手算的预期值对比。三步下来九成的问题都能定位。for i in range(1, 6): print(f[调试] i{i}) total i print(f[调试] total{total})调试信息记得加统一前缀方便定位也方便调完一次性删掉。用print调试不丢人我到现在写复杂循环时也这么干。7.3 把练习改成小工具让代码有去处纯练习的代码写完就扔记忆留存率其实不高。真正记得牢的是那些被反复用到的代码。所以我建议把练习往“小工具”方向改猜数字改成能选择难度区间的版本质数判断改成能输出某个范围内所有质数的版本冒泡排序改成能按字典某个键排序的版本。改完之后放进一个自己的脚本文件里下次要用直接翻出来。我自己有个叫practice.py的文件里面攒了几十个小函数从is_prime到“统计一段文本里每个单词出现的次数”都是练习时写的。写项目时遇到类似需求经常能直接搬过去改两行就用。这种“练习有去处”的感觉比任何学习计划都更能让人坚持下去。最后分享一个我自己一直在用的检验方法任何一段循环代码写完先不运行拿张纸把前三次迭代和最后一次迭代的变量值写出来再运行对答案。对得上说明思路清楚对不上说明某个边界你其实没想明白。这个方法比起直接运行然后看结果猜原因效率高得多尤其是嵌套循环手推一遍之后对代码的理解会完全不一样。