ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python循环语句详解:从for/while到break、continue的完整学习路径

Python循环语句详解:从for/while到break、continue的完整学习路径

学Python最让人感到“开始有点爽”的节点,我个人的体会是从循环语句开始的。前面学变量、学数据类型,都是在认识砖块和水泥,到了循环,你才算真正开始盖房子。就拿最常见的一个场景来说:你想打印1到100的数字,没有循环,你得写print(1)、print(2)……这样写下去写到第100行,而你学会for循环之后,短短几行代码就搞定了。这就是循环语句最直观的杀伤力——它能帮你处理所有“重复做某件事”的需求。本篇文章围绕Python循环语句的学习进行一次系统拆解,适合刚开始接触编程语法的新手,也适合学了一段时间但总是理不清for和while边界、遇到死循环就慌的同学。我会把循环相关的核心语法、执行过程、避坑经验全部掰开揉碎讲清楚,看完之后你完全可以照着写、照着练。

我见过很多学习者卡在循环这一步,原因其实不是笨,而是没搞清楚循环的本质。循环本质只有一句话:让计算机反复执行一段代码,直到某个条件发生改变。这句话听起来很简单,但一旦和break、continue、嵌套这些机制混在一起,新手就开始懵。这篇内容会从for到while,再从三大控制语句到常见报错与排查方法,梳理出一条完整的学习路径,跟着走一遍基本就能把循环系统搞明白。

1. for循环:我最推荐的遍历神器

1.1 range函数与数字序列

Python里最常见的循环写法就是for循环。很多从C语言转过来的朋友第一次看到for i in range(10)这种写法时会有点不适应,因为C语言的for循环写法是for(int i = 0; i < 10; i++),条件、初始化、步进全写在一起。Python的for循环则更像是“遍历”,它会逐个取出一个序列里的元素,然后让变量依次等于这些元素。range(10)生成的就是一个从0到9的数字序列。

for i in range(10): print(i)

这段代码会打印0、1、2一直到9,一共10个数字,不会打印10。这个细节很重要,很多刚接触的同学第一次跑的时候都会困惑“为什么没有10?”,其实是因为range(n)生成的是[0, n)这个区间,左闭右开。

range函数还有两个进阶用法:range(start, stop)和range(start, stop, step)。比如要打印100以内的偶数,可以用range(0, 100, 2),从0开始每次加2,到98结束。再比如要倒着打印10到1,可以用range(10, 0, -1),注意当步长为负数时,stop值也是开区间,所以想打印到1就得把stop写成0或写成1都可以,要看具体写法,实操中我会写range(10, 0, -1)来打印10到1。

关于range到底返回的是一个怎样的对象,Python 3里range返回的是range对象,它是个可迭代类型,不是列表。你要是直接print(range(10)),会看到输出range(0, 10)而不是[0, 1, 2...],想转成列表需要包一层list()。不过在实际使用中,我们大多数情况下直接把它丢进for循环里遍历就行,不需要转列表。

1.2 遍历字符串、列表与字典的常用姿势

for循环真正的魅力在于:凡是能放进去遍历的对象,它都能逐个取出来。

字符串可以遍历,每次取一个字符:

name = "python" for ch in name: print(ch)

拿到的是p、y、t、h、o、n这六个字符。这个特性在处理文本时非常方便,比如统计一个字符串里某个字符出现了几次,直接遍历字符串然后逐个判断就行。

列表遍历最常见:

students = ["张三", "李四", "王五"] for student in students: print(student)

这个写法直观到不需要解释。但到字典这里,就容易搞混了。直接for key in dict拿到的其实是字典的键,不是键值对。如果你想让循环同时拿到键和值,需要用dict.items()方法:

scores = {"语文": 90, "数学": 85, "英语": 88} for subject, score in scores.items(): print(subject, score)

items()返回的是每个键值对元组,在循环里用两个变量subject, score去分别接收键和值,这叫元组解包。类似的用法还有dict.keys()只拿键,dict.values()只拿值,按需取用。

1.3 for循环的嵌套与else分支

嵌套循环是很多初学者的一道小坎。所谓嵌套,就是在循环体里面再写一个循环,比如乘法表:

for i in range(1, 10): for j in range(1, i + 1): print(f"{j} x {i} = {i * j}", end="\t") print()

外层的i控制行数,内层的j控制一行里打印几列,end="\t"让每个乘法结果之间用制表符而不是换行分隔,内层循环结束后用print()换一行。这段代码的输出就是标准的上三角乘法表。嵌套循环的逻辑重点在于外层循环每走一次,内层循环要完整地全部走一遍,理解了这句话,嵌套循环就没有秘密了。

还有一个比较冷门但偶尔会用到的语法:for...else。当循环正常结束(没有被break打断)时,执行else块中的代码:

for i in range(5): if i == 3: break else: print("循环没有被中断")

上面这个例子因为i等于3时触发了break,所以else块不会执行。这个语法在写“查找是否存在某个元素”这类场景里很好用。比如要检查一个列表里有没有负数,用一个for循环扫描列表,如果找到负数就break,如果整个扫描完都没有负数,就进入else分支做对应的处理。这种写法比用一个标志位变量去记录“是否找到”要简洁不少,代码的可读性也更好。

2. while循环:条件被满足之前,一直干下去

2.1 while循环的执行逻辑与基本语法

for循环擅长处理可迭代对象,而while循环更适合处理“我不知道要循环多少次,只知道什么时候该停下来”的场景。它的语法结构非常简单:

while 条件表达式: 循环体代码

执行过程可以这样理解:每次进入循环前,先判断条件是否为真,为真就执行循环体,执行完再跳回开头重新判断条件,条件为假就跳出循环,继续执行后面的代码。

来看一个最简单的计数器:

i = 1 while i <= 5: print(i) i += 1

这段代码打印1到5。注意最后一行i += 1,不少初学者第一次写while循环的时候会忘掉这行,结果程序跑成了死循环。因为这个条件i <= 5永远为真,i永远不会变大,循环永远结束不了。写while循环的时候,一定要确认循环体中有某个操作能改变条件表达式的值,否则就是死循环。

2.2 交互式循环:不知道循环次数时的经典案例

while循环最经典的scenario就是与用户交互。比如写一个猜数字的小游戏,让用户不断输入数字,直到猜对才退出:

secret = 42 while True: guess = int(input("请输入你猜的数字:")) if guess == secret: print("恭喜你,猜对了!") break elif guess > secret: print("大了,再试试") else: print("小了,再试试")

这个例子用了while True这种写法,本身是个无限循环,靠break在特定条件满足时跳出。这种“先无限跑,遇到退出条件再break”的模式,在开发中是相当常见的,比如写一个"输入exit退出"的命令行小工具就很顺手:

while True: command = input("请输入指令:") if command == "exit": print("再见!") break print(f"执行指令:{command}")

相比for循环,while循环的条件掌控感更强,适合处理轮询、等待状态变化、持续采集数据这类需求。在实际项目里,比如从传感器读取数据直到指定阈值、重试网络请求直到成功,这些都是while循环的看家本领。

2.3 while与else的搭配以及它和for的边界

跟for一样,while也可以搭配else使用,同样表示“循环体没有被break中断时会执行else块”:

i = 1 while i < 3: i += 1 else: print("循环正常结束")

这个知识点了解即可,实际项目中while...else的出场率远低于for...else,我写代码这么长时间,while...else用的次数一只手数得过来。

那么for和while到底怎么选?我给一个很实用的判断标准:如果你要处理的对象是一个明确的数据集合(列表、字符串、字典、数字区间),用for;如果你是根据条件决定循环次数,比如某个变量的值达到了某个阈值就停,用while。这个标准基本覆盖了90%以上的场景。还有一种情况,你想写一个“死循环”直到某事件发生,那明显是while True更自然。把这两句话记在脑子里,选起来就不会纠结了。

3. break、continue与pass:控制循环流的三个关键工具

3.1 break:跳出整个循环

break的作用是立即终止当前整个循环,不管循环现在执行到第几次,也不管条件还没不满足,直接跳出循环体,继续执行循环之外的代码。

来看一个用途:在列表里找第一个能被7整除的数:

numbers = [3, 5, 8, 14, 21, 28] for num in numbers: if num % 7 == 0: print(f"找到了:{num}") break

这个循环从头扫描列表,找到14的时候触发break,打印结果后立即停止循环,21和28不会再被检查。如果没有break,循环会一路跑到底。这种“找到就停”的写法处理搜索类需求时效率很高,避免不必要的后续遍历。

注意break只能跳出离它最近的一层循环,这个点特别关键。如果你在嵌套循环的内层写break,它只会终止内层循环,外层循环还会继续跑。举例来说:

for i in range(3): for j in range(3): if j == 2: break print(i, j)

内层循环在j等于2时跳出,但外层循环没有受影响,会继续执行i从0到2的全部过程。如果想从内层循环直接跳出所有外层循环,单靠break做不到,一般会用标志位或者把逻辑封装进函数用return返回。

3.2 continue:跳过本次迭代

continue的作用和break完全不同:它不会终止整个循环,只是跳过当前这一轮剩下的代码,直接进入下一轮循环。

看个过滤负数的例子:

data = [3, -1, 5, -2, 8] for num in data: if num < 0: continue print(num)

这个循环遇到负数时直接跳到下一个元素,负数不会被打印。结果输出3、5、8。在数据清洗的场景里,continue几乎是标配操作——遇到脏数据跳过,保留有效数据做后续处理。

写continue的时候,有一点容易让人困惑:如果循环体里还有其它的计数逻辑,continue会跳过这些逻辑,直接进入下一轮。所以除非你确实想把remainder of this iteration全部跳过,否则别图省事把不该跳过的语句写在continue后面。

3.3 pass:占位符的艺术

我见过不少同学刚学pass的时候很疑惑:“我没看懂pass是什么用?”其实pass就是一个什么都不做的占位语句。它不是为了控制逻辑,而是为了保持语法完整性。

为什么需要它?因为Python的循环体、函数体不能完全为空,如果你写了if条件还没想好做什么,或者循环逻辑还没写完,不放任何语句会直接语法报错。这时候放一个pass进去:

for i in range(10): pass # 先占位,等会儿再写逻辑

这个代码不报错,但什么也不干。拿来做占位符、临时跳过还没实现的逻辑,非常实用。写大型项目时分步开发,局部逻辑还没写到位,先用pass占住位置,后面再回来补,这个用法我觉得是pass最核心的价值。有些初学者会想着用注释来代替pass,但实际上注释并不能弥补语法结构,该放pass的时候还得放。

3.4 三个控制语句的对比与应用场景

语句作用循环是否继续典型场景
break立即终止整个循环终止找到目标元素、退出交互式输入
continue跳过本次迭代继续下一轮数据过滤、跳过异常数据
pass什么都不做正常继续占位符、保持语法完整

这三者在同一个循环体里是可以搭配使用的,比如for...if...elif...else分支里分别用break、continue、pass,控制复杂的逻辑流向。我第一次在实际项目里同时使用这三个的时候,是一个数据处理脚本:循环读取每一条记录,遇到非法格式就continue跳过,遇到结束标志就break退出,遇到暂时不处理的类型就用pass占位等后续版本再补充。一套下来逻辑非常清晰,也推荐新手多尝试这种组合的写法。

4. 循环进阶:enumerate、zip与推导式组合用法

4.1 enumerate:在遍历时同时拿到索引和值

在Python中遍历列表时,如果你不仅关心元素本身,还需要它的下标位置,很多人第一反应是写个计数器变量:

i = 0 for name in students: print(i, name) i += 1

这种写法倒不算错,但不够优雅,而且容易忘记手动更新i导致索引错漏。用enumerate一行搞定:

for i, name in enumerate(students): print(i, name)

enumerate同时返回索引和元素,元组解包后直接分别赋值给i和name。还可以指定起始索引:

for i, name in enumerate(students, start=1): print(i, name)

这个写法在需要生成“第几个数据”信息时特别好用,比如写日志、生成序号、按行处理文件时,拿到的索引直接就能用。

4.2 zip:并行遍历多个序列

有时候你有两个长度相等的列表,想按位置对齐同时遍历,比如一组姓名和一组分数:

names = ["张三", "李四", "王五"] scores = [90, 85, 88] for name, score in zip(names, scores): print(name, score)

zip会按位置把多个序列的元素“打包”成一个个元组,循环里解包后就可以分别使用。这个功能写起来非常顺手,比起“用下标访问两个列表”的方式更简洁,也不容易把下标写错。

如果两个列表长度不一致,zip会以短的序列为准,多出来的元素会被丢弃。如果需要以长的为准,可以用itertools.zip_longest来处理缺失部分。这算是zip耗时较久后才会踩到的深水区,先记下即可。

4.3 while循环下的zip等价操作

其实自己用while也能模拟一个“并行遍历”的效果:

names = ["张三", "李四", "王五"] scores = [90, 85, 88] i = 0 while i < len(names) and i < len(scores): print(names[i], scores[i]) i += 1

这个写法与zip的效果一致,但代码明显更长。理解这个等价关系非常重要——它帮助你搞懂zip背后做的事情:其实就是在维护索引,按索引同时从几个序列取值。真正理解这个底层逻辑后,你便会更欣赏zip这种优雅的封装。

4.4 列表推导式:很多循环可以简化成一行

列表推导式说起来其实不是循环语句,但它能替代很多简单的for循环场景。举个例子,生成1到10的平方列表:

squares = [x ** 2 for x in range(1, 11)]

这一行代码等价于:

squares = [] for x in range(1, 11): squares.append(x ** 2)

列表推导式还支持条件过滤:

even_squares = [x ** 2 for x in range(1, 11) if x % 2 == 0]

这一行生成了2、4、6、8、10的平方列表。如果你之前只知道用for循环加if判断来写,这个写法直接简化了一半以上的代码量。

同样还有字典推导式:

squares_dict = {x: x ** 2 for x in range(1, 6)}

生成{1: 1, 2: 4, 3: 9, 4: 16, 5: 25}。集合推导式、生成器推导式也存在,不过初学者先掌握列表推导式,后面遇到需要传入函数参数或做内存优化的时候再扩展学习就来得及。

5. 循环里的经典坑:死循环、误修改迭代变量与大循环性能

5.1 死循环:while里最容易踩的坑

前面提过,while循环里忘了更新循环变量会造成死循环。死循环的实际表现就是程序一直在跑,光标一直闪,界面卡住不结束。我在实际调试时也见过不少死循环案例,除了忘记更新条件变量,还有一类是将条件写反了导致永远满足。

排查思路很有规律:进入死循环先别慌,直接在代码里插入print打印关键变量的值,看它有没有在变化。比如:

i = 1 while i <= 100: print(i) # 忘了写 i += 1

在循环体里打印i以后你马上就会发现i一直等于1,条件永远为真。找到问题后再补上更新语句就好了。如果实在不好确认是哪一行导致的问题,就疯狂加print,一行一行缩小范围。这是排查死循环最朴素也最有效的方式。

另一个经典错误:=写成==导致条件恒真。比如:

while i = 5:

这行代码在Python里直接语法报错的,不像C语言可以隐式赋值然后判断。Python在这点设计上比较安全,不用太担心。但理论上仍可能出现赋值表达式walrus operator的场景,初级内容不多展开,只需要尽量少在条件里修改变量就好。

5.2 循环中修改正在遍历的列表

在for循环遍历列表时直接删除列表元素,是另外一个很隐蔽的坑。看一个例子:

lst = [1, 2, 3, 4, 5] for x in lst: if x % 2 == 0: lst.remove(x) print(lst)

你可能预期结果只剩1、3、5,但实际结果是[1, 3, 5]——运气好的时候是你要的结果,但处理稍微复杂一点的列表时结果就会变得很奇怪。比如[1, 2, 3, 4, 6, 8]这种列表,一边遍历一边删除会导致一些偶数被跳过。

为什么会这样?因为for循环在内部按索引位置迭代,删除一个元素之后,后面的元素会往前移动,而循环的索引计数器不会自动后退,于是就会跳过紧接着的下一个元素。

解决办法有几种。最简单的做法是创建一个新列表,把符合条件的元素收集到里面:

lst = [1, 2, 3, 4, 5] new_lst = [] for x in lst: if x % 2 != 0: new_lst.append(x) print(new_lst)

另一种做法是利用“遍历副本”的思路,循环遍历列表的副本,操作源列表,这样索引就不会乱:

lst = [1, 2, 3, 4, 5] for x in lst.copy(): if x % 2 == 0: lst.remove(x)

推荐新手使用“新建列表收集结果”的方式,逻辑最直白,也不容易踩坑。等到编程熟练以后甚至可以直接改用列表推导式,一行解决:

lst = [x for x in lst if x % 2 != 0]

5.3 循环中修改变量值得注意的问题

还有一类问题:在for循环里擅自修改循环遍历的那个变量,其实没什么用,反而容易引起误导。比如:

for i in range(5): i = 10 print(i)

这段代码打印5个10,但循环本身还是按原计划迭代5次,下一次迭代时i重新赋值为序列里的下一个值。就算你能拿到赋值后的值也是临时的,下一轮直接重置。所以修改循环变量本身几乎没有什么正面作用,还会让读代码的人一头雾水。循环变量的意义是“当前取到的值”,不要试图去改变它来控制循环走向,等循环全部跑完之后,变量会保留最后一次迭代的值,这是正常的。

基于这个原理,还有一种更隐蔽的常见误区是用循环变量参与计算后想累计起来,但忘记先声明一个普通变量来累加,结果只保留最后一次的结果。比如:

total = 0 for x in range(1, 101): total = total + x print(total)

total在循环外部定义,每次循环用total + x更新总累加值,循环结束后total就是1加到100的结果5050。如果把total = 0放在循环里,每次循环都会把total重置为0,最后结果只有100。这类“循环变量的归属范围”问题,新手经常搞混,值得在练习时多花点时间理解。

5.4 循环性能与不必要的计算

说到循环性能,先说结论:Python的for循环本身并不慢,关键在于循环体里做了什么。如果你在循环体里做大量无谓的重复计算或频繁调函数,性能就会差很多。

一个常见优化是:把不随循环变化的计算提到循环外面。假设有一段代码需要循环遍历每个元素并累加math.sqrt的值,它等效于直接sqrt每个值,但如果循环外能先算的就不该在循环内每轮重算。比如闭区间里一个常量在循环里反复求一次幂,倒不如把常量结果算好放在变量里,循环里直接用这个变量。

另外一个优化是减少在循环里对列表的append、字典的键查找次数,多使用局部变量,少使用全局变量,这样能省下不少属性查找开销。不过这些优化属于性能调优范畴,初学者暂时不需要过度关注,先把逻辑写对,等真的遇到性能瓶颈再考虑优化也不迟。过早优化在工程上是种浪费。话虽如此,对“在循环里随手做大量无关计算”这种习惯还是要有意识地避免,养成好习惯没必要等踩了坑再改。

6. 新手的两个经典练习与完整实现

6.1 打印九九乘法表:嵌套循环的最佳入门练习

九九乘法表几乎是所有Python学习者都会接触的练习,因为它完美覆盖了嵌套循环、格式化输出、步长range这几个知识点。上面已经给出过最终的代码,这里再复盘一遍完整的思考路径。

我先想外层循环管行,内层循环管列。第n行应该输出“n”的那一档乘法:1 x n、2 x n一直到n x n。所以外层i从1到9,内层j从1到i。每个乘法结果用制表符分隔,同一行内不换行,一行结束后换行。用字符串格式化控制输出格式,对齐更美观:

for i in range(1, 10): for j in range(1, i + 1): print(f"{j} x {i} = {i * j}", end="\t") print()

这段代码的每一行输出都经过format对齐,end="\t"让结果在终端里排列得整整齐齐。我推荐新手上手循环时一定要亲自把这段代码敲一遍,然后尝试改成打印倒三角版本,条件改成j in range(i, 10)即可,这个小的变换会让你对角标控制的理解上升一个台阶。

6.2 猜数字游戏:while交互式循环的完整练习

猜数字游戏综合运用了while循环、break、input输入、条件判断好几块内容。我想给一套完整代码,里面加一点简单的次数统计,让练习更有意思:

import random secret = random.randint(1, 100) count = 0 print("我已经想好了一个1到100之间的数字,来猜猜看吧!") while True: guess = int(input("请输入你猜的数字:")) count += 1 if guess < secret: print("猜小了,往大点猜") elif guess > secret: print("猜大了,往小点猜") else: print(f"恭喜你猜对了!你一共猜了{count}次") break

这个程序的核心思路是:先通过random.randint生成一个随机数,然后进入while True循环接收用户的猜测,判断大小给出提示,直到用户猜中触发break退出循环。用户输入字符串时int转换有可能会报错,那属于异常处理的内容,还没学到的话可以先忽略,这个练习的重点是通过边读边猜理解while True + break的循环模式。

6.3 用while循环实现计数器:理解条件边界的实战

很多教材会用“打印1到100”为例讲for循环,却很少讲用while实现的版本。其实用while实现一遍计数器会极大加深你对循环两个关键词条件变化的理解:

i = 1 while i <= 100: print(i) i += 1

这跟for i in range(1, 101): print(i)是等价效果。区别在于,for循环的“步进”由range内部机制负责,而while循环需要手动i += 1。正是这个“手动”提醒了我们要思考:循环的每个边界是怎么变化的,条件什么时候会不成立。建议你把这个代码跑一遍,然后改成打印100以内所有奇数、打印1到100的和、倒着打印50到1,多练几个小变体,循环的基本功就扎实了。

7. 循环学习过程中的常见问题速查与实战吐槽

7.1 为什么我的程序卡住了不输出任何东西?

多半是死循环。重点检查while的条件表达式,确认循环体里是否更新了条件相关的变量;如果是for循环,检查是不是在遍历一个永远不会结束的迭代器。解决方式很有规律:先在循环体里加打印语句确认变量的变化情况,定位卡住的位置,再把逻辑修正。还有一个常识:IDE卡死不代表程序真的运行失败,有时也可能只是输出太多刷屏导致界面没响应,这种情况ctrl+c中断程序就行。

7.2 循环里用了break,但还是没跳出循环?

极大可能是break写在嵌套循环的内层,导致只跳出内层循环。上面说过break只能跳出最近的循环,如果是嵌套循环要跳出全部,可以考虑用返回或标志位。这里再补一个简洁的写法:可以把逻辑包进函数,在找到结果时直接从函数里return返回,用“一级跳出全部”的方式取代多层break。

def find_item(): for i in range(10): for j in range(10): if i * j == 42: return i, j return None print(find_item())

这个方法非常有用,是我实际项目里最常用的“跳出多层嵌套”手段。

7.3 循环里更新的变量在循环结束之后取什么值?

循环结束后,循环变量会保留最后一次迭代的值。比如:

for i in range(5): pass print(i) # 输出4

这是一个容易让人意外的点,但它确实就是这么设计的。在while循环中,循环变量的残留值也同样存在。这块如果没搞清楚,后面在调试时看到“循环外用了循环变量”可能会丈二和尚摸不着头脑。

7.4 无限循环到底有没有用?

有,而且很有用。比如服务端监听、持续数据采集、实时信号处理都会用while True加上break的模式。设计数据库连接池的时候,重试逻辑也会用while循环去不断尝试。担心死循环不丢人,真正重要的是理解循环的终止条件是什么,能用break、return等方式保证程序在正确的时间点退出。

7.5 关于大循环性能我的建议

如果你的循环要跑几十万上百万次,优先考虑这几个方向:一是尽量用局部变量,减少循环体内的全局变量访问;二是能用内建函数和库函数处理的逻辑,尽量用现成的方法,比如列表排序用sorted,求和用sum,不要手动写循环累计;三是比较重的计算如果与循环无关,提升到循环外;四是真的遇到超大数据量,考虑用NumPy等向量化方案替代纯Python循环。这些其实需要等学到后面才能完全消化,现在先有个意识即可。

我个人在实际操作中的体会是:循环这种东西,光看永远学不会,必须亲手写、亲手调试、亲手踩坑,才能真正形成肌肉记忆。你可以从今天开始,试着把日常生活中的重复事务抽象成循环模型,比如“统计这一周每天的运动步数”“逐个处理手机里的未读消息”这些场景,然后在草稿纸上画出循环的过程图,再翻译成Python代码。等for和while的边界、break和continue的作用、嵌套循环的顺序这些都亲自确认过一遍之后,你写Python的底气会和之前完全不同。这个内容后续还可以这样扩展:把循环语句和列表、字典结合,再往后加上函数封装,循环语句会渗透到你写的每一个Python项目里。先把这几个基础动作练好,后面写代码的路会越走越顺。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进