直接写正文,不加说明。 Python的循环语句,是很多人从“会写脚本”跨到“写干净代码”的一道坎。老实说,我在初学的头一两个月里,循环用得最多的地方就是遍历列表打印结果,直到后来写了一个批量重命名文件的小工具,才真正意识到循环不是“反复执行几遍”那么简单——它背后是代码的组织方式、数据的遍历方式、还有程序在什么时机跳出、什么时候兜底。这篇文章就把循环的完整细节梳一遍,从for和while的底层思路,到range参数、else子句、嵌套循环的坑,再到推导式和迭代器怎么替代循环,适合把Python语法看过一遍、但写循环时总感觉不踏实的同学,也适合想系统梳理循环知识点的人。
1. 为什么循环是Python学习的分水岭
1.1 循环解决的是“怎么重复”的问题
程序里最常见的需求不是计算1+1,而是“对一批数据做同样的处理”。比如有100个文件名要加前缀,有10个网页要抓标题,有20个字段要校验。循环的本质就是把“重复”这件事交给机器,让人只需要写一次处理逻辑。
很多初学者一开始写代码,会习惯复制粘贴。比如:
print("第1次") print("第2次")这当然能跑,但一旦次数变成变量,或者要处理的数据来自外部输入,复制粘贴就完全失去意义。循环的价值不只是省代码,而是让程序在处理“数量未知”的数据时依然稳定。你会慢慢发现,所有跟批量、轮询、累积、搜索相关的逻辑,几乎都离不开循环。
Python里有两种主力循环:for和while。严格来说还有嵌套循环、循环里的break和continue、循环的else子句,但核心骨架就是这两种。理解了它们的差异,很多场景下你都能马上判断该用哪种。
1.2 用生活场景理解for和while
我常给身边的朋友打一个比方:for循环像“按清单挨个处理”,清单上有多少项,就处理多少项,处理完自然结束。while循环像“守着某个条件,条件为真就一直做”,比如等地铁,等车来了就上车,没来就继续等。
对应到代码里,for循环适合遍历已知序列:列表、字符串、字典、文件行。while循环适合需要反复判断条件的场景:重试连接、等待输入、倒计时。当然Python里两者有很多时候可以互换,但选对了会让代码更好读,也更容易避免死循环。
举个例子,你要对列表里的每个数字加1:
nums = [1, 2, 3] for i, n in enumerate(nums): nums[i] = n + 1这里用for很自然。但如果你想不断接收用户输入,直到用户输入exit才停止,用while就比for清晰:
while True: cmd = input("输入命令(exit退出): ") if cmd == "exit": break等你自己写多了就会发现,for是“把一件事做完”,while是“只要条件满足就一直做”。这两种语义上的差别,决定了代码的表达力。
2. for循环:Python里最常用的循环
2.1 for循环的基本语法
Python的for循环和C语言差别很大。C语言里for通常写成for(int i=0;i<n;i++),Python里for更像“foreach”,直接遍历可迭代对象中的元素。基础结构如下:
for item in iterable: # 处理item这里的item会依次被赋值为iterable里的每个元素。iterable可以是列表、元组、字符串、字典、集合、文件对象,甚至生成器。
我见过不少刚转Python的人,一开始会纠结“我怎么拿到下标”。因为如果直接for n in nums,只能拿到值,拿不到序号。但Python提供了一个自带函数enumerate,可以同时拿到下标和值:
fruits = ["苹果", "香蕉", "橘子"] for idx, fruit in enumerate(fruits): print(idx, fruit)输出:
0 苹果 1 香蕉 2 橘子如果你确实只关注下标,也可以写成for i in range(len(fruits)),但这通常没必要。直接用for fruit in fruits更符合Python的遍历习惯,只有在需要同时修改列表元素或者访问相邻元素时,才考虑用下标。
2.2 range的用法与参数细节
range可能是for循环里最常搭配的函数。它生成的是左闭右开区间。看几个例子:
range(5) # 0, 1, 2, 3, 4 range(2, 5) # 2, 3, 4 range(0, 10, 2) # 0, 2, 4, 6, 8 range(10, 0, -2) # 10, 8, 6, 4, 2range最多接三个参数:起点、终点、步长。终点不包含在结果里,这是新手最容易踩的坑。比如想打印1到5,有人会写range(1, 5),结果少了5,应该写range(1, 6)。为了记住这点,我习惯把它理解为“到终点就停,但不到终点”。
步长还可以是负数,可以实现倒序。不过倒序遍历通常直接用reversed()更好读:for i in reversed(nums):。range还有一个细节是它返回的对象不是列表,而是range对象,特点是你只能遍历它,不能直接修改它。如果你非要一个列表,包一下list(range(10))就行。
for循环里的range最常见的用途就是“重复做N次操作”:
for _ in range(100): do_something()这里下划线代表“我不关心循环变量的值”,单纯想执行100次。这个写法很Pythonic,而且能够避免定义出一个用不到的变量。
2.3 遍历字符串、列表、字典的实战
字符串本身是可迭代对象,所以可以直接for遍历得到每个字符:
word = "hello" for ch in word: print(ch)列表的遍历最简单,直接for item in items。但有个细节:如果要在遍历过程中删除元素,直接删除会导致跳项或者索引错乱。经典做法是遍历副本,然后删原列表:
items = ["a", "b", "c", "d"] for item in items[:]: if item == "b": items.remove(item)更推荐的方式是用列表推导式重建列表,后面会讲。字典的遍历方式稍微丰富一些,默认遍历的是键:
d = {"name": "张三", "age": 20} for key in d: print(key, d[key])还有三种常见写法:
for key in d.keys(): # 键 for value in d.values(): # 值 for key, value in d.items(): # 键值对遍历字典的键值对时,注意变量名要成对出现,否则容易ValueError。另外,遍历字典的过程中不要直接修改字典的大小(新增或删除键),那会触发RuntimeError。我踩过这个坑,后来都是先复制键列表,再遍历键列表修改字典。
3. while循环:条件控制的灵活用法
3.1 while循环的结构
while循环的结构更简单:
while condition: # 循环体只要condition为真,循环体就会一直执行。每次执行完成后重新计算condition,如果还是真,继续执行,直到condition为假。
这里最需要警惕的是死循环。写while时必须确保循环体里存在能改变条件的手段,比如计数器递增、输入状态改变、break触发。
count = 0 while count < 10: print(count) count += 1一旦忘了count += 1,程序就会无限打印0,最后只能强制中断。还有另一种情况:条件本身是无穷真的,比如while True。这种写法本身没错,但循环体内部必须有break出口,否则就是真正的死循环。
有一种常见的疑问:“while这个条件能不能写成复杂表达式?”当然可以,比如:
while len(queue) > 0 and not stop_flag:使用and、or、not组合多个条件都行。要注意运算符优先级,拿不准就加括号。
3.2 经典场景:轮询与重试
很多实际场景都适合用while。最典型的是重试机制。比如请求某个外部服务,可能临时失败,你希望最多重试3次,每次间隔2秒:
retries = 0 max_retries = 3 while retries < max_retries: try: fetch_data() break except Exception: retries += 1 time.sleep(2)另一种是交互式菜单:
choice = "" while choice != "q": choice = input("请输入选项(q退出): ") handle(choice)这类场景如果非要用for硬套,写起来会别扭。while表达“直到满足某个停止条件”更贴切。还有一种稍隐晦的用法:把while当作“哨兵循环”,用一个标记变量控制退出,比如done = False,循环体内处理完某个任务后设置done = True。这在读文件、事件处理中很常见。
4. break、continue、else:循环的控制与兜底
4.1 break和continue的差异
break是立即终止整个循环,不管后面还有没有迭代次数。continue是跳过当前这一次迭代的剩余代码,直接进入下一次迭代。两者的区别,一个类比“退出整个会议”,一个类比“出去接个电话再回来继续开会”。
示例:
for i in range(10): if i == 3: break print(i) # 输出 0 1 2for i in range(5): if i == 2: continue print(i) # 输出 0 1 3 4使用break时要注意,它只跳出当前这层循环。如果你在嵌套循环里想跳出两层,break只能让你退出内层循环,外层还会继续。解决这个问题的常见方案是设置一个标志位,或者使用“提前return”封装成函数。我不建议滥用标志位,逻辑变得很绕,很多时候可以抽出独立函数直接return。
continue也有个注意点:在while循环里,continue会直接跳到条件判断,不会执行continue之后的语句,所以循环变量的更新如果写在最后,会跳过,导致死循环。
i = 0 while i < 3: i += 1 if i == 2: continue print(i)如果你把i += 1放在print之后,遇到continue就不自增了,i一直不变,死循环。因此写while时,我习惯把计数器更新放在条件判断之前或者放在continue之前,反正要确保continue不会跳过更新步骤。
4.2 循环的else子句
循环的else是Python一个很有特点的语法,很多其他语言没有。它的含义是:如果循环正常结束(没有被break打断),就执行else块。如果循环被break跳出,else块不执行。
for n in range(2, 10): for x in range(2, n): if n % x == 0: break else: print(n, "是质数")这里内层循环如果找到因子就break,不执行else;如果没有找到因子,整个循环跑完,else就会执行。这个写法避免了“flag”标记变量,能直接判断一个数是质数。初看觉得有点反直觉,但用多了会觉得非常顺手。
while也有else:
i = 0 while i < 3: i += 1 else: print("循环正常结束")需要提醒的是,循环的else含义跟if-else完全不同,很多人初次见时容易理解错。记住核心判断依据是“有没有break”。
5. 嵌套循环与综合案例
5.1 嵌套循环的消耗与优化
嵌套循环就是循环里套循环。最典型的情况是双层循环遍历二维数据,比如矩阵、坐标。示例:
matrix = [ [1, 2, 3], [4, 5, 6], ] for row in matrix: for cell in row: print(cell)嵌套本身没有问题,但要警惕三层以上,以及大数据量下出现的性能问题。如果外层循环有m次,内层循环有n次,总执行次数就是m×n。要是三层嵌套就是m×n×k。哪怕每次操作很快,数量一上来也会卡顿。
我在实际写代码时,如果发现有两层以上的嵌套循环,会先思考能不能减少内层循环的次数。比如一个常见场景:两个列表a和b,要找出同时出现的元素。最粗暴的方法是双层循环:
for x in a: for y in b: if x == y: result.append(x)这个写法时间复杂度O(n*m)。如果a和b都很大,就会很慢。更好的办法是转成集合做交集:
result = list(set(a) & set(b))代码更短,速度也快得多。嵌套循环不是不能用,而是要知道它贵在哪,并且能想到替代方案。
5.2 案例:打印九九乘法表
九九乘法表是练习嵌套循环最经典的题目。基本思路是外层循环控制行数(1到9),内层循环控制当前行中的列数(1到行号),在打印时用格式化控制对齐。
for i in range(1, 10): for j in range(1, i + 1): print(f"{j}x{i}={i*j}", end="\t") print()这个代码有两个关键点,第一个是内层range的终点是i + 1,因为range不包含终点,所以当i=1时内层只有j=1,当i=9时j从1到9。第二个是end参数,默认print结尾换行,改成制表符就能保持同一行,等内层循环结束后再打一个空print换行。
运行结果就像我们小时候背的口诀表。除了这个基础版,还可以练习倒序、上三角、下三角等变体,来加深对嵌套循环控制的理解。比如加上格式宽度:
print(f"{j}x{i}={i*j:<4}", end="")这个:<4表示左对齐占4个字符宽度,输出更整齐。
6. 实用技巧、避坑清单与性能提示
6.1 常见错误与排查
写循环时最常踩的坑,我整理成一张清单:
| 错误场景 | 现象 | 解决办法 |
|---|---|---|
| range终点写错 | 少遍历一次 | 记住左闭右开,或使用len直接配合下标 |
| while忘记更新条件变量 | 死循环 | 确保循环体内有i+=1或break |
| 遍历列表时删除元素 | 漏处理/索引错位 | 遍历副本或改用推导式 |
| continue跳过循环变量更新 | while死循环 | 把变量更新提到continue之前 |
| break只跳出内层循环 | 外层继续执行 | 用函数return或标志位控制 |
| 遍历字典时修改键 | RuntimeError | 先list(d.keys())再遍历 |
排查循环问题还有一个通用思路:先在循环开头打印每次的变量值,但数据量大时别打印太多。更好的方式是缩小范围,比如把range(1000)改成range(5),先确认逻辑正确再放大数据。
6.2 生成器、推导式与循环替代
Python里很多循环其实可以被推导式替代,写法更简洁。列表推导式的基本格式:
new_list = [expression for item in iterable if condition]比如:
# 原来的循环 squares = [] for i in range(10): squares.append(i ** 2) # 推导式 squares = [i ** 2 for i in range(10)]推导式简化了“创建列表-循环-append”三行代码,而且可读性很好,只要条件不太复杂。类似的还有字典推导式:
d = {key: value for key, value in pairs}生成器表达式则是把方括号换成圆括号,得到的不是列表,而是生成器。它按需计算,适合大数据量场景,避免一次性占用太多内存:
total = sum(i ** 2 for i in range(1000000))这里用生成器做惰性求和,比先构建一个100万元素的列表再求和要求内存小得多。
虽然推导式能替代一部分循环,但并不是所有循环都适合推导式。如果循环体里有多行逻辑、需要break或修改其他变量,强行写推导式会很丑。我个人的判断标准是:if还是单个表达式,且逻辑能一行说清楚,才用推导式。
再提一个常见误区:推导式本质还是一个循环,嵌套推导式也一样。例如[ (x, y) for x in range(3) for y in range(3)]执行次数是3×3=9次,它并不比双层for快,只是写起来短。高性能场景下,该用算法优化还是得优化,不能指望用推导式解决性能问题。
6.3 性能实测与思路
很多人关心“for和while哪个更快”。在CPython里,循环本身的性能差异其实不大,真正的消耗在循环体执行的操作上。但有几条实践心得:
第一,优先使用内置函数而不自己写循环。比如累加一个列表的和,自己写循环for x in nums: total += x,不如直接用sum(nums)。内置函数底层是C实现,不仅快,还更简洁。类似的还有max、min、any、all、map、filter等,能直接用就不手写。
第二,减少循环体内的重复计算。比如:
for i in range(1000): value = expensive_function(i) # 如果expensive_function不依赖i,应该提到循环外如果函数昂贵,先算一次放在循环外。另外一个常见优化是提前计算列表长度,避免每次循环都调用len,虽然len本身开销小,但习惯上还是要减少不必要的重复操作。
第三,用局部变量引用全局函数或变量。例如在循环里经常用到math.sqrt,可以先sqrt = math.sqrt,再在循环里用sqrt(...),这样省去每次查找全局名字的开销。这个优化幅度不大,但在数据量极大的时候,效果可以感受得到。
第四,循环里拼接字符串不要用+=,特别是字符串较长、次数较多时。字符串在Python里是不可变对象,每次+=都会生成一个新字符串,效率差。典型场景是拼接10000个字符串,用列表收集再"".join(list)会快得多。这也是for循环中很容易踩到的性能坑。
从实际工作来看,循环性能优化要优先提升算法复杂度,而不是纠结微小的语法差异。把一个双层循环改成集合操作,提升的是数量级;把循环里的字符串拼接换成join,提升的是常量级。先把能改算法的地方改掉,再去抠细节。
我个人在实际操作中还有一个习惯:写任何循环时,都会问自己三个问题。第一,我是在遍历一个已知的集合吗?如果是,for更自然。第二,我是在等待某个条件吗?如果是,while更合适。第三,这个循环能不能用内置函数或推导式替代?如果可以,优先替代。这套判断流程看起来简单,但对新人尤其管用,能够帮你从一开始就写出更干净的循环代码。
最后分享一个小技巧:如果你经常需要循环遍历多个序列,用zip可以成对遍历,更优雅:
names = ["张三", "李四", "王五"] scores = [88, 92, 76] for name, score in zip(names, scores): print(name, score)另外,如果只是想让循环“空转”一会儿,比如简单延迟,可以用for _ in range(1000000): pass。但要注意,这种忙等待会占用CPU,如果真的是等待,优先用time.sleep。
循环的知识点不算多,但组合起来能产生很丰富的逻辑。把for和while的区别想透,把break、continue、else的语义记牢,再动手写几个带嵌套、带中止条件的Demo,你的Python基本功就扎实了一大截。后面不管是写爬虫、数据处理,还是写点自动化脚本,循环这一关过了,其他语法学起来会顺很多。