1. 循环的本质先搞明白这3个问题再动手写Python写了这么多年我见过太多新手在循环上栽跟头。其实循环本身并不难难的是很多人没想清楚“为什么需要循环”“什么时候用哪种循环”“循环怎么停下来”这三个问题就埋头写代码结果写出来的循环要么跑不出结果要么跑的压根不是自己想要的结果。今天这篇就把Python循环的4大核心用法——for循环、while循环、循环控制、循环嵌套——从头到尾捋一遍包括我实际调试代码时踩过的坑和总结出来的经验希望能帮你一次把循环这块地基打扎实。先说第一个问题循环到底解决了什么问题简单说就是“重复”。任何一段逻辑只要你需要重复执行就可以考虑用循环。比如遍历列表里的每个元素、逐行读取文件、反复计算直到满足某个条件这些都是循环的典型场景。但要注意循环不是万能的有些重复用函数递归更清晰有些重复用列表推导式更简洁搞清楚边界才不会把代码写成一团乱麻。第二个问题for和while怎么选我给一个非常朴素的判断标准——如果你知道要循环多少次用for如果你不知道要循环多少次只知道什么时候停止用while。举个例子“打印1到100”这种明确次数的用for而“一直读用户输入直到输入exit”这种就得用while。这个标准虽然简单但在实际开发里覆盖了90%以上的场景。第三个问题也是最容易翻车的循环怎么停下来for循环本身有明确的遍历范围一般不会出现死循环。但while循环如果不小心把退出条件写错或者漏掉更新退出条件的语句程序就会卡在那里一直转CPU风扇呼呼响。我在初学阶段就干过这事——写了个while来累加变量结果忘了在循环体里给计数器加1程序直接跑死最后只能强制终止。这个问题后面第6章会详细讲排查方法但你在写循环之前心里一定要清楚“这段循环凭什么停下来”。2. for循环Python最常用的遍历利器2.1 range()函数的完整用法for循环在Python里几乎是万能遍历器而range()函数则是它最亲密的小伙伴。range()的基本用法是range(start, stop, step)从start开始到stop结束不包含stop本身步长为step。很多人刚接触时容易踩的第一个坑就是“stop不包含”比如range(1, 5)生成的数字是1、2、3、4没有5。这个设计其实是为了配合索引从0开始的习惯Python里所有的切片操作都遵循左闭右开原则range只是遵守了同样的规矩。实际写代码时range()有几个实用变体值得记住。比如range(5)默认从0开始生成0到4range(2, 10, 2)生成2、4、6、8这些偶数range(10, 0, -1)可以倒序生成10到1。值得注意的是range()在Python 3里返回的是range对象而不是列表它并不一次性把所有数字都生成出来而是惰性求值。这意味着即使是range(10**9)也不会占用海量内存这一点在性能敏感场景下非常有用。还有一个容易被忽略的点range()可以配合len()来遍历列表的索引。这种写法在需要同时使用索引和值的时候很常见fruits [apple, banana, orange] for i in range(len(fruits)): print(i, fruits[i])不过说实话这种写法在现代Python里已经不算最优了因为有enumerate()可以更优雅地实现同样的效果。后面第5章里我会专门讲。2.2 遍历字符串、列表与元组for循环遍历可迭代对象是它的看家本领。字符串、列表、元组都是可迭代对象直接for迭代即可。遍历字符串时每次取到的是一个字符遍历列表时每次取到的是一个元素遍历元组时逻辑上也完全一样。这个机制的原理其实在于Python的迭代协议——对象实现了__iter__()方法返回一个迭代器for循环不断地调用这个迭代器的__next__()方法直到抛出StopIteration异常。我见过不少初学者在遍历列表时想靠索引手动控制写成了这样fruits [apple, banana, orange] i 0 while i len(fruits): print(fruits[i]) i 1这种写法虽然也能跑通但完全没有必要。直接用for遍历列表代码更短、可读性更好、执行效率也更高。为什么因为Python的for循环直接操作迭代器底层走的是C级别的迭代协议而while加索引的方式每轮循环都要执行len()函数调用和索引取值多了一层Python层面的开销。2.3 遍历字典的三种姿势字典的遍历是个高频场景而且有讲究。字典本身是可迭代对象但直接for循环遍历字典时拿到的是键而不是值。这个设计经常让新手困惑。我总结下来遍历字典有这三种常用姿势第一种遍历键直接用for key in dict:。第二种遍历值用for value in dict.values():。第三种同时遍历键和值用for key, value in dict.items():。在绝大多数场景里我强烈建议用.items()。因为它一次性把键值对都拿到既避免了用键再去查值的二次查找开销又让代码结构更清晰。你可能觉得一个字典查找也没什么但在循环里每一轮都做一次dict[key]查找累计下来性能差异是实打实的。我在处理一个几百万条数据的字典时从key迭代加索引查找改成items()直接迭代运行时间肉眼可见地降了下来。另外注意一点Python 3.7之后字典保持插入顺序这意味着你遍历字典时拿到的键的顺序就是插入时的顺序。如果你需要有序遍历直接用字典本身就可以不需要额外排序。2.4 同步遍历多个序列zip单个序列遍历很简单但实际开发里经常需要同时遍历两个甚至多个序列。比如有两个列表一个存姓名一个存分数想成对打印出来。最简单直观的写法是按下标索引names [张三, 李四, 王五] scores [88, 95, 72] for i in range(len(names)): print(names[i], scores[i])但更Pythonic的写法是用zip()for name, score in zip(names, scores): print(name, score)zip()的原理是把多个可迭代对象按索引对应位置打包成一个个元组然后交给for循环去解包。它有几个特性需要注意第一当传入的可迭代对象长度不一致时zip()默认以最短的那个为准多余的元素直接丢弃。第二zip()返回的也是一个迭代器不是列表。如果你需要一次性拿到所有打包结果可以用list(zip(...))把它转换成列表。之前我写过一段处理学生信息的代码两个列表长度不一致用zip()遍历时就出现了“静默丢弃”的问题——有几个学生的分数信息莫名没被处理。排查半天最后发现是数据源本身就不完整zip()按最短长度匹配直接把后面的元素丢掉了。这个场景如果你需要严格对齐可以用itertools.zip_longest()它会以最长的序列为准缺失的部分用fillvalue填充默认是None。这个坑我建议你提前知道不然数据对不上的时候真的很难排查。3. while循环条件驱动型的灵活玩法3.1 while的基本结构与退出条件while循环的结构比for简单直白只要条件为真就一直执行循环体。它的适用场景是“不知道要循环多少次但知道什么时候停”。这个“什么时候停”就是退出条件也是while循环的灵魂。count 0 while count 5: print(count) count 1这段代码输出0、1、2、3、4。关键在于第4行的count 1这就是“更新退出条件”。如果把这行删掉count永远是0条件count 5永远为真循环就成了死循环。我见过太多死循环的bug都源于“忘了更新退出条件”或者“更新的逻辑写错了”。写while循环时心里一定要时刻绷着一根弦每一次循环体执行完条件相关的变量有没有向“退出”的方向迈进如果没有这个循环就准备跑一辈子了。3.2 while与for的选型对照为了更直观地帮你在写代码时做选型决策我整理了一个对照表这是我在实际教学和代码评审中最常用的判断依据判断维度for循环while循环循环次数已知或可预测未知靠条件判断遍历对象可迭代对象列表/字典/字符串等一般无固定遍历对象退出机制遍历完自动结束条件为假时结束典型场景遍历、计数、处理集合用户输入轮询、游戏循环、状态机死循环风险低高需谨慎设置退出条件简单说能用for解决的问题尽量别用while。for循环语义清晰、边界明确、天然避免死循环可读性也好。但我不是说while没用相反有些场景只有while能优雅搞定。最典型的比如实现一个“猜数字”游戏你根本不知道用户要猜几次才中只能用while去反复读取用户输入直到猜对为止。还有一类场景是“事件循环”比如GUI程序的主循环、游戏引擎的帧循环本质都是while True配合内部的状态判断来退出。这种写法虽然看起来像死循环但循环体内有break或有条件地改变状态变量实际是受控的。3.3 循环中的哨兵模式while循环有一个经典用法叫“哨兵模式”专门用来处理“读到某个特殊值就停止”的场景。这个特殊值就叫哨兵。最典型的例子是读取用户输入直到输入某个特定值退出while True: line input() if line quit: break process(line)这个例子里break扮演了退出循环的角色。相比把退出条件写在while后面这种“条件在循环体中间”的写法灵活性更高因为你可以在处理数据前做各种校验再决定是否退出。哨兵模式还有另一种写法用变量做标记。比如遍历一个列表遇到第一个满足条件的元素就退出这种场景下可以把标志变量和break配合使用。不过说实话Python里能用break就不用标志变量因为break的意图更直接不会出现“标志变量忘了更新”的尴尬情况。4. break、continue与else循环控制的三板斧4.1 break提前终止循环break的用途是立即终止整个循环不管循环条件还成不成立。它常用来做“提前退出”。比如在一个列表里查找第一个满足条件的元素找到后就没必要继续遍历了nums [3, 7, 2, 9, 5, 11, 4] for n in nums: if n 10: print(找到了:, n) break这样一旦找到大于10的数循环立刻停止不会白白遍历完整个列表。这对性能的价值在数据量大时体现得很明显——比如一个百万级的列表你要找的目标恰好排在前面break能省掉后面几十万次无用的迭代。break在嵌套循环里的行为要特别留意它只能终止当前最内层的那个循环。如果两层循环嵌套break只能跳出内层外层还会继续执行。这个我在5.1节里会结合实例细说。4.2 continue跳过本轮剩余代码continue和break的差别在于break是“整个循环都不干了”continue是“本轮剩下的代码不干了下一轮继续”。比如打印1到10之间的奇数for i in range(1, 11): if i % 2 0: continue print(i)当i是偶数时continue会让本轮循环提前结束print(i)这行就不会执行直接进入下一轮i3。这样写比if i % 2 ! 0: print(i)多了一层判断但对于“本轮后面有一大段代码”的场景来说continue的价值就非常明显了——少一层嵌套代码平铺直叙可读性更好。要注意的是continue在while循环里同样有效但它只跳过本轮剩余代码不会跳过对退出条件的更新。如果你在while循环里把更新退出条件的语句写在continue后面那一旦执行continue退出条件就永远不会更新又是一个死循环的坑。4.3 for…else与while…else的隐藏用法这个可能是Python里被误解最多的语法。for...else和while...else的规则其实很简单如果循环正常结束没有被break打断就执行else块如果循环被break打断就不执行else块。这个特性在实际开发里非常好用。比如你要检查一个数字是否在列表中如果不使用for...else语法通常要定义一个标志变量nums [2, 4, 6, 8] found False for n in nums: if n 6: found True break if not found: print(没找到)用了for...else代码就清爽很多for n in nums: if n 6: break else: print(没找到)这个语法我第一次接触也觉得别扭但用习惯之后是真香。它的核心价值在于把“循环是否正常结束”这个状态流转成了代码结构不用额外维护标志变量。我自己的经验是在“在集合中搜索元素”这种场景里for...else比标志变量方案更不容易出错因为它把“找到”和“没找到”的后续逻辑天然地分开了。5. 循环嵌套与进阶写法从笨办法到优雅写法5.1 嵌套循环的经典案例循环嵌套就是循环里面再套循环最常见的是双重for循环。经典的九九乘法表就用它实现for i in range(1, 10): for j in range(1, i 1): print(f{j}x{i}{i*j}, end\t) print()外层循环控制行数1到9内层循环控制每行打印多少列1到i注意range(1, i1)里的i是外层循环的当前值这是九九乘法表“三角形”形状的关键。嵌套循环的逻辑顺序一定要搞清楚外层循环每走一步内层循环要完完整整地跑完一整轮。很多人刚开始写嵌套循环时容易把内外层搞混我建议你用print加变量的方式去观察执行顺序或者直接在纸上画一个2x2的表格手动跑一遍程序流程比盯着代码猜要快得多。嵌套循环的性能问题要重视。双重循环的时间复杂度是O(n²)一旦数据量上去运行时间会急剧增长。比如两个各有1000个元素的列表做全量匹配就是100万次操作。像这种场景如果可以用集合的in操作来替代内层循环性能可以提升几个数量级。5.2 列表推导式循环的替代写法列表推导式是Python里最常用的循环替代方案。它的基本语法是[表达式 for 变量 in 可迭代对象 if 条件]把for循环、条件过滤和结果收集压缩成一行。比如生成1到10的平方数squares [i * i for i in range(1, 11)]这行代码等价于squares [] for i in range(1, 11): squares.append(i * i)我不否认for循环在某些情况下更直观但列表推导式的优势在于它更简洁、执行速度更快底层有优化而且表达的是“我要什么”而不是“怎么装列表”语义层次更高。在数据处理和项目开发中能用推导式的地方我会优先用推导式。列表推导式还可以配合if条件和多个for循环。比如生成两个列表的笛卡尔积就写两层forpairs [(x, y) for x in [1, 2] for y in [a, b]]生成的结果是[(1, a), (1, b), (2, a), (2, b)]。不过这里有个度的问题超过两层的推导式可读性会急剧下降我建议嵌套层数超过两层就回归到普通for循环别为了炫技牺牲代码的可维护性。除了列表推导式还有字典推导式和集合推导式原理一样只是把方括号换成花括号。三种推导式在数据清洗、批量转换、过滤取数等场景非常实用是每个Python开发者都应该熟练掌握的进阶技巧。5.3 enumerate与索引操作很多场景下你既需要值也需要索引这时候enumerate()是最佳选择。它接受一个可迭代对象返回一个每次产出索引和值的迭代器。我在2.1节里提到过用range len但有了enumerate之后那种写法基本可以淘汰了。fruits [apple, banana, orange] for idx, fruit in enumerate(fruits): print(idx, fruit)enumerate()还支持指定起始索引。比如从1开始编号for idx, fruit in enumerate(fruits, start1): print(idx, fruit)这个小参数在打印序号时非常实用默认从0开始加上start1就自动变成了“第1个、第2个”的自然语言编号。还有一个在循环中操作索引的进阶技巧如果你想在遍历时同时修改列表的多个元素尽量避免在循环内部通过insert或remove来改结构因为这会改变列表长度和索引极易引发“跳过元素”的诡异bug。正确的做法是先遍历把需要修改的索引记下来循环结束后再统一修改。这个我第6章会展开讲。6. 这些循环坑我踩过排查与避坑实录6.1 死循环的快速排查法死循环是while循环里最常见的事故。一旦程序卡住不输出CPU占用飙高基本就是死循环了。我排查死循环的方法是先看退出条件涉及哪些变量然后在循环体里print这些变量的变化过程。i 0 while i 100: print(i) # 观察i的变化 # 假设这里忘了i 1运行之后你会看到i一直是0永远小于100这就说明退出条件没有被推进。这种排查方法的本质是把程序执行的中间状态暴露出来你肉眼就能看到到底是哪里没有更新。更专业的做法是给循环设置“保险丝”——一个最大运行次数的保护。比如i 0 max_iters 10000 while True: i 1 if i max_iters: raise RuntimeError(循环疑似死循环)在调试阶段加这种保护能帮你快速定位问题避免程序卡死。等确认逻辑没问题了再把保护去掉。6.2 遍历时修改序列的致命错误这是Python循环中最高频的隐蔽bug。很多新手想在一个循环里删除列表中的某些元素写出了这样的代码nums [1, 2, 3, 4, 5, 6] for n in nums: if n % 2 0: nums.remove(n)运行结果是[1, 3, 5]看起来好像没问题但如果你用nums [1, 2, 4, 5, 6]再试一次结果就会变成[1, 4, 5]6没有被删掉。原因在于循环是按原列表的索引顺序遍历的但你在遍历过程中删了元素列表变短了后面的元素索引整体前移循环却还按原来的索引继续走这就导致某些元素被跳过。正确的做法是先创建一个副本去遍历或者用列表推导式生成新的列表nums [1, 2, 3, 4, 5, 6] nums [n for n in nums if n % 2 ! 0]列表推导式的好处是它生成一个全新的列表原列表在这个过程中完全不被修改从根源上消除了“边遍历边修改”的问题。如果非要在原地修改那就先收集要删除的索引循环结束后再统一处理。6.3 循环性能优化的三个经验循环是性能优化的重点区域。一个程序如果大部分时间都在跑循环那么循环的每一毫秒都值得抠。我总结三个最实用也最有效的优化思路第一尽可能把能移出循环的操作移出去。比如某个函数调用在循环里每次都算同一批数据那么完全可以在循环外面先算好再进来用。len()、一些常量的计算都是典型例子。第二用局部变量加速。在循环体内访问局部变量比访问全局变量快。实际做法是如果循环体里要反复用到某个全局变量可以在循环前赋值给一个局部变量循环体里用这个局部变量。这个优化在数据量大时提升明显。第三能用Python内置函数或内置数据结构就尽量用。比如“判断元素是否在集合中”用set的in操作是O(1)但在列表里用in操作是O(n)。这个差距在数据量大时是质的差异。我写过一段代码原本用列表做去重判断5万条数据跑得极慢换成set之后性能提升了上百倍。处理这类循环性能问题思路不是“优化循环内部代码”而是“用更合适的数据结构彻底消除循环”。另外再提一句循环体里尽量别用print。print本身有IO开销在循环里高频print会严重拖慢程序。调试时可以打印正式运行时如果非要在循环里输出信息建议控制打印频率。我个人在实际调试循环时还有一个习惯先写一个极端小测试用例比如只有3个元素的列表循环跑一遍手动追踪每一步的变量变化确认逻辑没问题再上真实数据。别小看这一步它能帮你隔离出绝大多数逻辑错误远比在大数据上排查要快。你写循环代码的时候如果能保持这个习惯相信会少走很多弯路。