写 Python 代码不卡壳的,十个里有九个都倒在循环嵌套上。单个 for 循环谁都会写,闭着眼睛都能遍历一个列表;一旦变成两层甚至三层循环,脑子就开始转不过弯来:到底哪层先跑,哪层后跑,缩进一格两格什么区别,break 到底停的是哪个循环。这不是你笨,而是循环嵌套本身就需要一种"分层思考"的脑回路,没训练过的人突然面对它,卡壳太正常了。这篇文章就把 for 循环嵌套这件事掰开揉碎讲清楚。我会从最底层的执行顺序讲起,用几个必须练的经典案例带你走一遍完整过程,再把缩进、变量命名、break/continue 归属这类最容易踩的坑全部列出来,最后聊聊嵌套循环的性能问题和进阶写法。无论你是刚学 Python 的零基础新手,还是学了一段时间但看到多层循环就发怵的自学者,这篇文章都应该能帮你把这块硬骨头啃下来。1. 为什么需要循环嵌套:一个"重复中的重复"问题1.1 单个循环解决不了的事先想一个问题:如果让你打印一个三行三列的星号方阵,长这样:*** *** ***用单个 for 循环怎么写?你可能会说,那就 print 三次"***"呗,代码就是:print("***") print("***") print("***")这只解决了"三行"的问题。如果要打印十行十列呢?虽然可以复制粘贴十次,但这显然不是写代码该有的样子。更麻烦的是,如果列数也要跟着行数变,比如第一行 1 颗星、第二行 2 颗星、第三行 3 颗星,那你连"复制粘贴模板"都不好使了,因为每一行的内容都不一样。这个时候就会发现,问题的本质是:每个行都需要执行一遍"打印一行星号"这个动作,而"打印一行星号"这个动作本身又需要循环多次拼接字符。行是一个重复,行内部的字符又是一个重复。两个重复叠在一起,单层循环就无能为力了,必须用循环嵌套——外层循环管行,内层循环管列。1.2 嵌套循环的执行顺序:外层走一步,内层走一整圈理解循环嵌套最关键的一点,是搞明白它的执行节奏。很多人第一次接触的时候会理所当然地以为"先执行完外层,再执行内层",这是最大的误解。真实的执行顺序是:外层循环每执行一次,内层循环都要完整地从头跑一遍。打个比方:内层循环像分钟,外层循环像小时。时间走一个小时,分钟要转整整一圈;时间走 60 个小时,分钟就得转 60 圈。外层"迈出一步",内层"绕场一周"。我画过无数遍这个流程,用文字描述就是:for i in range(3): # 外层循环 for j in range(3): # 内层循环 print(i, j)这段代码的执行顺序是:i 拿到 0,进入外层循环体碰到内层循环,j 从 0 开始跑:打印 (0, 0)j 变 1,还在内层循环里:打印 (0, 1)j 变 2,打印 (0, 2)j 的 range(3) 用完了,内层循环结束外层循环体也执行完了,i 变 1再次进入外层循环体,又碰到内层循环,j 重新从 0 开始:打印 (1, 0)、(1, 1)、(1, 2)i 变 2,内层循环再来一遍,打印 (2, 0)、(2, 1)、(2, 2)i 的 range(3) 也用完,整个嵌套循环结束注意到没有?内层循环的 j 每一次都是从 0 重新开始的。它不会因为上一次已经跑到 2 就继续从 3 开始。这是嵌套循环和普通循环最不一样的心理模型。1.3 用"行与列"的思维看待嵌套循环理解了执行顺序之后,再来看为什么嵌套循环在编程里无处不在。几乎所有二维结构的东西,都能用嵌套循环来"扫描"一遍:打印图案:外层管行,内层管列二维列表/矩阵:外层遍历行,内层遍历行里的每个元素批量处理数据:外层遍历每个文件,内层遍历文件中的每行记录组合枚举:外层遍历第一个集合,内层遍历第二个集合,生成所有组合只要你在一个循环体里面还要接着重复做某事,第一反应就应该是:这里可能需要嵌套循环。它是一种"降维打击"的工具,把原本需要写 N 遍的重复代码,压缩成几行结构化的逻辑。我在教学过程中发现一个规律:凡是能熟练用"行和列"来预演嵌套循环运行过程的同学,后面学算法、学数据处理都会顺畅很多;凡是硬背代码、不深究执行顺序的同学,换个题目就懵。所以这一节的内容真的值得多花点时间吃透。2. 核心细节:缩进、变量与执行节奏2.1 缩进决定结构,结构决定命运Python 里缩进不是装饰,是语法的一部分。循环嵌套的层级完全靠缩进来表达。同样是两行代码,缩进不同,逻辑完全不同:for i in range(3): for j in range(3): print(i, j) print("内层结束啦")和for i in range(3): for j in range(3): print(i, j) print("内层结束啦")第一段代码里,print("内层结束啦")和for j in range(3):是同一级缩进,所以它是外层循环体的一部分,内层循环每完整跑完一遍,它才执行一次——总共执行 3 次。第二段代码里,它和内层循环里的print(i, j)是同一级缩进,所以它属于内层循环体,每打印一对 (i, j) 就会执行一次——总共执行 9 次。就这一个缩进的位置差异,结果差了 3 倍。说它是"一字千金"毫不夸张。提示:写嵌套循环的时候,建议把代码编辑器的缩进显示功能打开,用空格缩进就统一用空格,用 Tab 就统一用 Tab,千万别混用。我见过太多报错IndentationError: unexpected indent的新手,一查全是混用空格和 Tab 惹的祸。2.2 内外两层循环的变量职责:别混用,别污染嵌套循环里通常会有两个循环变量,外层一个,内层一个。常见的约定是外层用i,内层用j,三层就再加一个k。这个约定不是强制性的,但它能帮你一眼看出当前的变量属于哪一层,读代码的时候不用动脑子。有些同学喜欢两层循环都用同一个变量名,比如这样:for i in range(3): for i in range(3): print(i, i)这段代码能跑,不会报错,但逻辑上已经乱了。内层的i会遮挡外层的i,外层循环的i在内层循环期间根本访问不到。如果后面你发现结果跟预期不一样,排查起来会非常痛苦。所以我的建议是:嵌套循环的每一层都有自己的循环变量,名字要有区分度。还有一个容易忽略的坑:内层循环里如果要引用外层的循环变量,一般没问题,因为内层作用域能访问外层变量;但如果在内层循环里对外层变量重新赋值,那就要特别小心。看这个例子:total = 0 for i in range(3): for j in range(3): total = total + i + j print(total)这个total没有歧义,它不受循环嵌套的控制,只是被内层循环反复修改。真正危险的情况是你在内层循环里改变了外层循环用来控制循环的变量,这会导致循环次数失控,甚至死循环。后面第四节我会专门讲这个坑。2.3 循环次数的计算:外层次数乘以内层次数嵌套循环的执行总次数是有明确数学公式的:如果没有 break、continue 这些中断语句,总的循环体执行次数 = 外层循环次数 × 内层循环次数。这个公式能帮你快速地估算一段嵌套循环的工作量。比如:count = 0 for i in range(10): for j in range(10): count += 1 print(count) # 10010 × 10 = 100 次。三层循环就是三个数相乘,10 × 10 × 10 = 1000 次。但要注意,这个公式的前提是内层循环的次数是固定值或者和 i 无关。实际编程中,内层循环的次数经常是依赖外层循环变量的,比如:count = 0 for i in range(10): for j in range(i, 10): count += 1 print(count) # 55这个例子中,内层循环的 range 起点是i,所以当 i=0 时内层跑 10 次,i=1 时跑 9 次……最后总数是 10+9+8+...+1 = 55 次,而不是 100 次。这种"内层次数依赖外层变量"的写法在打印三角形、处理矩阵上三角等场景中非常常见,计算次数时一定要按累加来算,不能想当然地直接相乘。2.4 列表数据里的嵌套遍历形态循环嵌套最常见的应用场景,是遍历二维列表。所谓二维列表,就是"列表里的元素还是列表",比如一个三行四列的"成绩表":scores = [ [88, 92, 79, 85], [95, 88, 91, 90], [78, 85, 83, 80] ]要打印所有成绩,几乎所有人的第一版代码长这样:for row in scores: for score in row: print(score, end=" ") print()外层循环遍历scores这个列表,每次拿到的row是其中一个子列表;内层循环遍历row,每次拿到的score是具体的成绩数字。这种"先取列表中的子列表,再取子列表中的元素"的两层结构,就是二维数据遍历的骨架。从切片、遍历到修改,所有二维数据处理都逃不开这种模式。我在后面实操环节会用一个更贴近实际场景的案例来演示,你马上就能感受到这种写法的价值。3. 实操:四个必练的经典案例这一节是本文的重头戏。我精选了四个最经典、最能练手、也最能覆盖循环嵌套各种细节的案例。每个案例我都会给完整代码、运行结果,以及关键解释。建议你打开编辑器,跟着敲一遍,绝对比盯着手机看有效果。3.1 九九乘法表:最经典的嵌套入门题九九乘法表几乎是所有编程教程都会提到的例子,因为它天然就是一个"行数递增、列数跟着变化"的结构,完美对应嵌套循环:for i in range(1, 10): for j in range(1, i + 1): print(f"{j} × {i} = {i * j:>2}", end=" ") print()运行结果:1 × 1 = 1 1 × 2 = 2 2 × 2 = 4 1 × 3 = 3 2 × 3 = 6 3 × 3 = 9 ...关键细节在第二行:range(1, i + 1)。因为第 i 行只需要打印到 i 为止,所以内层循环的右边界是i + 1。这个"内层循环范围依赖外层变量"的写法,是九九乘法表的核心考点。end=" "表示不换行,两个空格做分隔,print()空调用表示换行。我刚开始学 Python 的时候,九九乘法表抄了三遍才理解为什么内层要用i + 1而不是10。后来想通了:打印到第几列,取决于现在是第几行。这个"行决定列"的动态关系,就是循环嵌套里最需要建立的直觉。3.2 用星号画三角形与金字塔打印星号图案是另一个经典题型,它比乘法表更直观,也更考验对空格和星号数量的拆分能力。来看一个"右对齐直角三角形":n = 5 for i in range(n): for j in range(n - i - 1): print(" ", end="") for j in range(i + 1): print("*", end="") print()运行结果:* ** *** **** *****这个案例的精华在于:一层嵌套循环不够用了,需要两层内嵌循环——一层打空格,一层打星号。你能在这里看到嵌套循环不仅限于"两层",外层循环体里可以同时有多个并列的内层循环,它们按顺序依次执行。再来看一个完整的金字塔:n = 5 for i in range(n): # 空格数量:n - i - 1 for j in range(n - i - 1): print(" ", end="") # 星号数量:2 * i + 1 for j in range(2 * i + 1): print("*", end="") print()运行结果:* *** ***** ******* *********这里的两个数量关系是核心:空格是n - i - 1,星号是2 * i + 1。你可以代入 i=0 验证:第 0 行有 4 个空格、1 个星号;i=1 时有 3 个空格、3 个星号。这种"用数学关系控制输出"的能力,是画图案类题目的灵魂。我在带新人时发现一个高频错误:好多人会把空格循环放在星号循环后面,导致图案右半边凹了进去。记住,空格一定要在星号之前打印,因为输出是"先打印的内容靠左"。3.3 二维列表的遍历与求和图案类的嵌套循环练习的是"输出控制",接下来这个例子更贴近真实业务——处理二维数据。假设你有一个二维列表,存储了三家门店一周七天的销售额,你想算每天全部门店的总销售额,以及每家门店的周总销售额:sales = [ [1200, 980, 1350, 1100, 1500, 1650, 1400], # 门店A [800, 920, 1010, 870, 1100, 1200, 950], # 门店B [1500, 1420, 1280, 1350, 1600, 1750, 1880] # 门店C ] daily_total = [0] * 7 # 每天的全部门店总销售额 store_total = [0] * 3 # 每家门店的周总销售额 for i in range(len(sales)): # i 表示门店 for j in range(len(sales[i])): # j 表示星期几 value = sales[i][j] daily_total[j] += value store_total[i] += value print("每天全部门店总销售额:", daily_total) print("每家门店周总销售额:", store_total)运行结果:每天全部门店总销售额: [3500, 3320, 3640, 3320, 4200, 4600, 4230] 每家门店周总销售额: [9180, 6850, 10780]你看,一次双重循环,同时算出了两组不同的聚合数据。这里的手法很典型:内层循环每次能拿到一个具体元素,根据你"累加到哪个变量"来决定汇总维度。如果内层循环里同时往daily_total[j]和store_total[i]里累加,就相当于一次遍历,两种统计口径都得到了。这个案例我给很多零基础学员演示过,他们看完基本都能秒懂"为什么二维数据要用双重循环"——因为数据本身是二维的,你自然需要用两个下标来定位。这类业务型的例子比打印星号更有"代码感",也更接近真实开发。3.4 用一个批处理场景串联所有知识最后一个案例是一个完整的小项目,用来把前面所有知识串起来。场景是这样的:一家网店要对所有订单做批量结算,每个订单包含若干商品,你想打印出每个订单的金额明细和订单总金额。orders = [ [("键盘", 199), ("鼠标", 89), ("显示器", 1299)], [("U盘", 79), ("硬盘", 499)], [("耳机", 299), ("音箱", 459), ("麦克风", 399), ("声卡", 899)] ] for i, order in enumerate(orders, start=1): total = 0 print(f"订单 {i} 明细:") for name, price in order: print(f" - {name}: {price} 元") total += price print(f" 订单总金额: {total} 元") print("-" * 20)运行结果:订单 1 明细: - 键盘: 199 元 - 鼠标: 89 元 - 显示器: 1299 元 订单总金额: 1587 元 -------------------- 订单 2 明细: - U盘: 79 元 - 硬盘: 499 元 订单总金额: 578 元 -------------------- ...这个案例和前面商场销售的例子异曲同工,都是两层循环遍历"外层列表项、内层明细"。但这里多了一个重要的细节:total变量的初始化位置在外层循环体内、内层循环外。为什么?如果放在内层循环里,每个商品都会把 total 清零一次,最后算出来永远是最后一个商品的价格;如果放在外层循环外,所有订单的金额会混在一起,根本无法区分订单 1、订单 2。判断一个变量该放在哪一层,是嵌套循环里最容易出错的点,也是最值得认真体会的点。我常用的方法是:这个变量是想描述"外层对象的维度"还是"内层对象的维度"?描述订单的,就放在外层循环内;描述商品的,就放在内层循环内。4. 常见问题与排查技巧实录这一节的内容全部来自我实际面对过的报错现场。说真的,循环嵌套的报错几乎都是同样的几个原因反复出现,把它们记住了,排查起来就是分分钟的事。4.1 缩进错乱:逻辑错误的隐形元凶缩进问题有两种表现形式。一种是语法层面的,代码一跑就报IndentationError,这种反而好办,看报错提示的行号就能修。另一种更可怕的,是语法没问题、代码能跑、但结果完全不对——这种大多是因为缩进层级导致代码跑错了位置。举个例子,你想统计"每个子列表中大于 10 的元素个数":data = [[12, 5, 18], [3, 9, 15], [20, 7, 11]] counts = [] for sub in data: count = 0 for num in sub: if num > 10: count += 1 counts.append(count) print(counts) # [2, 1, 2]但如果把counts.append(count)的缩进往右挪一格,变成跟if同级:for sub in data: count = 0 for num in sub: if num > 10: count += 1 counts.append(count) # 错!每次符合条件的都 append 一次结果就会变成[1, 1, 1, 1, 1]这样的鬼东西。同样的代码,一个缩进位置之差,结果天差地别。注意:用 VS Code 或 PyCharm 的同学,强烈建议开启代码缩进可视化(显示空格和 Tab)。缩进错乱导致的 bug 很难直接"看"出来,但可视化之后,错误层级一目了然。4.2 在循环内部修改迭代变量:死循环制造机这是一个非常隐蔽的坑。前面说过,for 循环里用range的时候,修改i的值并不会影响循环的下一次取值,因为 range 已经生成好了一个序列,i每次只是从序列里取下一个值。但如果你在循环体里用了列表或类似结构做迭代对象,同时又修改了那个列表本身,就可能会引发意想不到的问题。至于 dead-loop,最常见的场景其实是 while 和 for 混用的时候。比如这样一段代码:i = 0 while i < 5: for j in range(3): print(i, j) # 忘了写 i += 1 → 死循环这是 while 循环的坑,不是 for 嵌套的坑。for 嵌套里真正常见的是这种:内层循环修改了外层用来控制循环次数的列表。比如:tasks = [1, 2, 3] for task in tasks: for i in range(2): print(task) if task == 2: tasks.append(999) # 迭代时修改了被遍历的列表运行这段代码,可能会卡死,也可能会一直追加下去,行为不可控。Python 的 for 循环是基于索引的,它在遍历的同时修改列表,会引发索引错乱,很多情况下会产生死循环或跳过元素。安全原则:不要在遍历列表的同时修改这个列表。如果确实想过滤或添加元素,建议先复制一份列表再遍历,或者改用 list comprehension 生成新的列表。这条经验从入门到进阶都适用。4.3 break 和 continue 到底作用于哪一层另一个高频困惑是 break 和 continue 的作用范围。很多初学者以为它们能让"最外层循环"也停下来,实际上它们的生效范围是当前所在的这一层循环。也就是说,break 写在哪个 for 里面,它就结束哪个 for。看这个例子:for i in range(3): for j in range(3): if j == 2: break print(i, j)运行结果是:0 0 0 1 1 0 1 1 2 0 2 1注意,每一行打两个数(j=0 和 j=1),到 j=2 时 break,但这里 break 只结束了内层循环。等内层循环 break 之后,代码回到外层循环,i 继续变 1、变 2,外层循环并没有停。如果你确实想在满足某个条件时"跳出一整个双层循环",那就要自己做一个控制标记:found = False for i in range(3): for j in range(3): if i * j == 6: found = True break if found: break print(i, j) # 2 3 吗?不对,条件是 i*j==6,结果是 (2, 3) 不成立,实际是 (2, 3) 才成立?注意 range(3) 只有 0,1,2这里我故意写了个容易混淆的例子,实际range(3)里不含 3,所以i * j == 6永远不会满足。写代码的时候一定要时刻注意 range 的区间。如果真需要跳出多层循环,Python 3.8 之后还可以考虑封装函数配合 return,逻辑会更清晰。这个"跳出多层循环"的需求,在实际业务里并不少见,值得记一下这个控制标记的写法。4.4 常见问题速查表现象可能原因解决方法内层循环一次就结束,外层也跟着结束了break 缩进位置错了,写在了内层外面检查 break 的缩进层级结果比预期少很多内层 range 的起点/终点写错代入几个 i 值验证内层循环次数结果完全重复内层循环没有使用外层变量确认内层逻辑是否需要依赖 i报错 list index out of range用下标遍历时误用了外层长度多用for item in list结构,少用下标数值统计比预期大累加变量初始化放在了外层循环外把累加变量移进外层循环内、内层循环外打印结果没有换行忘记在内层循环外调用print()内层结束后用一个空的print()换行这张表我建议收藏一下,写嵌套循环卡住的时候拿出来对照,比翻文档快得多。5. 循环嵌套的进阶玩法与性能提醒掌握了基础的嵌套循环,你的 Python 之旅已经能覆盖绝大多数二维数据场景了。但想写得又快又好,还有两个进阶点值得关注。5.1 用列表推导式替代简单的双重循环Python 有个特别的优势:很多简单的双层循环可以用列表推导式一行搞定。比如你想把一个二维列表"展平"成一维列表:matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] # 传统双重循环写法 flat = [] for row in matrix: for num in row: flat.append(num) # 列表推导式写法 flat2 = [num for row in matrix for num in row] print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9] print(flat2) # 和 flat 完全一样列表推导式里的for row in matrix for num in row看起来顺序很反直觉,但它确实就是嵌套循环的展开形式——第一个 for 对应外层,第二个 for 对应内层。写熟之后会很爽,但新手阶段我建议先用传统写法理解清楚,再迁移到推导式,否则容易把顺序搞反。5.2 嵌套深度与执行效率的关系前面说过,双层固定循环的总次数等于两层次数相乘。这个乘法增长很快:100 个数量的双层循环是 10000 次,三层就是 1000000 次。在刷题或处理大数据的场景里,把三层循环优化成两层,执行时间可能相差几十倍。举一个常见例子:假设你要在一个列表里找出两数之和等于某个目标值的所有数对。最直接的三层思维是"遍历所有 i、所有 j、再验证一次",但更好的写法是双循环就够,甚至用字典做到单循环。嵌套循环不是越多越好,能省一层就省一层。不过话说回来,对初学者而言,先保证代码正确,再优化效率。很多同学一上来就想着什么优化,结果写出了一堆看不懂的复杂代码,反而更慢。我见过一个学员把一段简单的双层循环改成用一堆位运算,跑是快了 20 毫秒,但代码他自己第二天都看不懂了。适量优化是好事,过度优化就是折腾。5.3 什么时候该把双层循环拆成单层有一类双重循环其实是"伪嵌套"——内外两层循环之间没有逻辑依赖,纯粹是因为数据是二维的。这种时候,Python 的itertools.product可以把嵌套循环扁平化成一个循环:from itertools import product for i in range(3): for j in range(4): print(i, j) # 等价写法 for i, j in product(range(3), range(4)): print(i, j)product(range(3), range(4))会生成 (0,0)、(0,1)、(0,2)、(0,3)、(1,0)……所有组合。它的作用是把两层循环的表达变成一层,逻辑上更线性,读起来更像"遍历一个集合的集合"。这种写法在你需要"嵌套枚举多个维度"时特别有用,比如生成两个列表的笛卡尔积。顺便说一句,product这类工具是 Python 标准库自带的,不需要额外安装任何东西,直接 import 就能用。注意:product适合维度较多、但每层之间无依赖的场景;如果内层循环范围依赖外层变量(比如九九乘法表),那还是老老实实用传统写法,硬套 product 反而更绕。结尾的话:嵌套循环的"肌肉记忆"怎么练最后分享一点我自己的体会。循环嵌套这个东西,本质上不是"会不会写代码"的问题,而是"能不能在脑子里预演循环过程"的问题。你不需要天赋异禀,只需要在初期刻意练习:每看到一个嵌套循环,先在纸上列出前几步的执行痕迹,算出总的执行次数,再看代码的输出是否和预演一致。练上 20 个例子,这个能力就长在你自己身上了。很多人学循环嵌套之所以痛苦,是因为急着背代码、急着跑结果,跳过了"预演"这一步。我对每一个来找我问嵌套循环的初学者都说同一句话:代码跑不出来不是问题,代码跑出来了但你看不懂它为什么这么跑,才是问题。你能把每一步执行顺序讲清楚,嵌套循环你就算真的掌握了。这套方法我已经带过好几波新人验证过,效果很稳定。你也不用一次练完所有案例,挑九九乘法表和订单结算那两个例子先上手,跑通之后再看星号图案和性能优化部分。等你能把一个三层循环的执行顺序在纸上完整写出来的时候,你回头看今天的困惑,就会觉得那只是一个小小的台阶而已。
写 Python 代码不卡壳的,十个里有九个都倒在循环嵌套上。单个 for 循环谁都会写,闭着眼睛都能遍历一个列表;一旦变成两层甚至三层循环,脑子就开始转不过弯来:到底哪层先跑,哪层后跑,缩进一格两格什么区别,break 到底停的是哪个循环。这不是你笨,而是循环嵌套本身就需要一种"分层思考"的脑回路,没训练过的人突然面对它,卡壳太正常了。这篇文章就把 for 循环嵌套这件事掰开揉碎讲清楚。我会从最底层的执行顺序讲起,用几个必须练的经典案例带你走一遍完整过程,再把缩进、变量命名、break/continue 归属这类最容易踩的坑全部列出来,最后聊聊嵌套循环的性能问题和进阶写法。无论你是刚学 Python 的零基础新手,还是学了一段时间但看到多层循环就发怵的自学者,这篇文章都应该能帮你把这块硬骨头啃下来。1. 为什么需要循环嵌套:一个"重复中的重复"问题1.1 单个循环解决不了的事先想一个问题:如果让你打印一个三行三列的星号方阵,长这样:*** *** ***用单个 for 循环怎么写?你可能会说,那就 print 三次"***"呗,代码就是:print("***") print("***") print("***")这只解决了"三行"的问题。如果要打印十行十列呢?虽然可以复制粘贴十次,但这显然不是写代码该有的样子。更麻烦的是,如果列数也要跟着行数变,比如第一行 1 颗星、第二行 2 颗星、第三行 3 颗星,那你连"复制粘贴模板"都不好使了,因为每一行的内容都不一样。这个时候就会发现,问题的本质是:每个行都需要执行一遍"打印一行星号"这个动作,而"打印一行星号"这个动作本身又需要循环多次拼接字符。行是一个重复,行内部的字符又是一个重复。两个重复叠在一起,单层循环就无能为力了,必须用循环嵌套——外层循环管行,内层循环管列。1.2 嵌套循环的执行顺序:外层走一步,内层走一整圈理解循环嵌套最关键的一点,是搞明白它的执行节奏。很多人第一次接触的时候会理所当然地以为"先执行完外层,再执行内层",这是最大的误解。真实的执行顺序是:外层循环每执行一次,内层循环都要完整地从头跑一遍。打个比方:内层循环像分钟,外层循环像小时。时间走一个小时,分钟要转整整一圈;时间走 60 个小时,分钟就得转 60 圈。外层"迈出一步",内层"绕场一周"。我画过无数遍这个流程,用文字描述就是:for i in range(3): # 外层循环 for j in range(3): # 内层循环 print(i, j)这段代码的执行顺序是:i 拿到 0,进入外层循环体碰到内层循环,j 从 0 开始跑:打印 (0, 0)j 变 1,还在内层循环里:打印 (0, 1)j 变 2,打印 (0, 2)j 的 range(3) 用完了,内层循环结束外层循环体也执行完了,i 变 1再次进入外层循环体,又碰到内层循环,j 重新从 0 开始:打印 (1, 0)、(1, 1)、(1, 2)i 变 2,内层循环再来一遍,打印 (2, 0)、(2, 1)、(2, 2)i 的 range(3) 也用完,整个嵌套循环结束注意到没有?内层循环的 j 每一次都是从 0 重新开始的。它不会因为上一次已经跑到 2 就继续从 3 开始。这是嵌套循环和普通循环最不一样的心理模型。1.3 用"行与列"的思维看待嵌套循环理解了执行顺序之后,再来看为什么嵌套循环在编程里无处不在。几乎所有二维结构的东西,都能用嵌套循环来"扫描"一遍:打印图案:外层管行,内层管列二维列表/矩阵:外层遍历行,内层遍历行里的每个元素批量处理数据:外层遍历每个文件,内层遍历文件中的每行记录组合枚举:外层遍历第一个集合,内层遍历第二个集合,生成所有组合只要你在一个循环体里面还要接着重复做某事,第一反应就应该是:这里可能需要嵌套循环。它是一种"降维打击"的工具,把原本需要写 N 遍的重复代码,压缩成几行结构化的逻辑。我在教学过程中发现一个规律:凡是能熟练用"行和列"来预演嵌套循环运行过程的同学,后面学算法、学数据处理都会顺畅很多;凡是硬背代码、不深究执行顺序的同学,换个题目就懵。所以这一节的内容真的值得多花点时间吃透。2. 核心细节:缩进、变量与执行节奏2.1 缩进决定结构,结构决定命运Python 里缩进不是装饰,是语法的一部分。循环嵌套的层级完全靠缩进来表达。同样是两行代码,缩进不同,逻辑完全不同:for i in range(3): for j in range(3): print(i, j) print("内层结束啦")和for i in range(3): for j in range(3): print(i, j) print("内层结束啦")第一段代码里,print("内层结束啦")和for j in range(3):是同一级缩进,所以它是外层循环体的一部分,内层循环每完整跑完一遍,它才执行一次——总共执行 3 次。第二段代码里,它和内层循环里的print(i, j)是同一级缩进,所以它属于内层循环体,每打印一对 (i, j) 就会执行一次——总共执行 9 次。就这一个缩进的位置差异,结果差了 3 倍。说它是"一字千金"毫不夸张。提示:写嵌套循环的时候,建议把代码编辑器的缩进显示功能打开,用空格缩进就统一用空格,用 Tab 就统一用 Tab,千万别混用。我见过太多报错IndentationError: unexpected indent的新手,一查全是混用空格和 Tab 惹的祸。2.2 内外两层循环的变量职责:别混用,别污染嵌套循环里通常会有两个循环变量,外层一个,内层一个。常见的约定是外层用i,内层用j,三层就再加一个k。这个约定不是强制性的,但它能帮你一眼看出当前的变量属于哪一层,读代码的时候不用动脑子。有些同学喜欢两层循环都用同一个变量名,比如这样:for i in range(3): for i in range(3): print(i, i)这段代码能跑,不会报错,但逻辑上已经乱了。内层的i会遮挡外层的i,外层循环的i在内层循环期间根本访问不到。如果后面你发现结果跟预期不一样,排查起来会非常痛苦。所以我的建议是:嵌套循环的每一层都有自己的循环变量,名字要有区分度。还有一个容易忽略的坑:内层循环里如果要引用外层的循环变量,一般没问题,因为内层作用域能访问外层变量;但如果在内层循环里对外层变量重新赋值,那就要特别小心。看这个例子:total = 0 for i in range(3): for j in range(3): total = total + i + j print(total)这个total没有歧义,它不受循环嵌套的控制,只是被内层循环反复修改。真正危险的情况是你在内层循环里改变了外层循环用来控制循环的变量,这会导致循环次数失控,甚至死循环。后面第四节我会专门讲这个坑。2.3 循环次数的计算:外层次数乘以内层次数嵌套循环的执行总次数是有明确数学公式的:如果没有 break、continue 这些中断语句,总的循环体执行次数 = 外层循环次数 × 内层循环次数。这个公式能帮你快速地估算一段嵌套循环的工作量。比如:count = 0 for i in range(10): for j in range(10): count += 1 print(count) # 10010 × 10 = 100 次。三层循环就是三个数相乘,10 × 10 × 10 = 1000 次。但要注意,这个公式的前提是内层循环的次数是固定值或者和 i 无关。实际编程中,内层循环的次数经常是依赖外层循环变量的,比如:count = 0 for i in range(10): for j in range(i, 10): count += 1 print(count) # 55这个例子中,内层循环的 range 起点是i,所以当 i=0 时内层跑 10 次,i=1 时跑 9 次……最后总数是 10+9+8+...+1 = 55 次,而不是 100 次。这种"内层次数依赖外层变量"的写法在打印三角形、处理矩阵上三角等场景中非常常见,计算次数时一定要按累加来算,不能想当然地直接相乘。2.4 列表数据里的嵌套遍历形态循环嵌套最常见的应用场景,是遍历二维列表。所谓二维列表,就是"列表里的元素还是列表",比如一个三行四列的"成绩表":scores = [ [88, 92, 79, 85], [95, 88, 91, 90], [78, 85, 83, 80] ]要打印所有成绩,几乎所有人的第一版代码长这样:for row in scores: for score in row: print(score, end=" ") print()外层循环遍历scores这个列表,每次拿到的row是其中一个子列表;内层循环遍历row,每次拿到的score是具体的成绩数字。这种"先取列表中的子列表,再取子列表中的元素"的两层结构,就是二维数据遍历的骨架。从切片、遍历到修改,所有二维数据处理都逃不开这种模式。我在后面实操环节会用一个更贴近实际场景的案例来演示,你马上就能感受到这种写法的价值。3. 实操:四个必练的经典案例这一节是本文的重头戏。我精选了四个最经典、最能练手、也最能覆盖循环嵌套各种细节的案例。每个案例我都会给完整代码、运行结果,以及关键解释。建议你打开编辑器,跟着敲一遍,绝对比盯着手机看有效果。3.1 九九乘法表:最经典的嵌套入门题九九乘法表几乎是所有编程教程都会提到的例子,因为它天然就是一个"行数递增、列数跟着变化"的结构,完美对应嵌套循环:for i in range(1, 10): for j in range(1, i + 1): print(f"{j} × {i} = {i * j:>2}", end=" ") print()运行结果:1 × 1 = 1 1 × 2 = 2 2 × 2 = 4 1 × 3 = 3 2 × 3 = 6 3 × 3 = 9 ...关键细节在第二行:range(1, i + 1)。因为第 i 行只需要打印到 i 为止,所以内层循环的右边界是i + 1。这个"内层循环范围依赖外层变量"的写法,是九九乘法表的核心考点。end=" "表示不换行,两个空格做分隔,print()空调用表示换行。我刚开始学 Python 的时候,九九乘法表抄了三遍才理解为什么内层要用i + 1而不是10。后来想通了:打印到第几列,取决于现在是第几行。这个"行决定列"的动态关系,就是循环嵌套里最需要建立的直觉。3.2 用星号画三角形与金字塔打印星号图案是另一个经典题型,它比乘法表更直观,也更考验对空格和星号数量的拆分能力。来看一个"右对齐直角三角形":n = 5 for i in range(n): for j in range(n - i - 1): print(" ", end="") for j in range(i + 1): print("*", end="") print()运行结果:* ** *** **** *****这个案例的精华在于:一层嵌套循环不够用了,需要两层内嵌循环——一层打空格,一层打星号。你能在这里看到嵌套循环不仅限于"两层",外层循环体里可以同时有多个并列的内层循环,它们按顺序依次执行。再来看一个完整的金字塔:n = 5 for i in range(n): # 空格数量:n - i - 1 for j in range(n - i - 1): print(" ", end="") # 星号数量:2 * i + 1 for j in range(2 * i + 1): print("*", end="") print()运行结果:* *** ***** ******* *********这里的两个数量关系是核心:空格是n - i - 1,星号是2 * i + 1。你可以代入 i=0 验证:第 0 行有 4 个空格、1 个星号;i=1 时有 3 个空格、3 个星号。这种"用数学关系控制输出"的能力,是画图案类题目的灵魂。我在带新人时发现一个高频错误:好多人会把空格循环放在星号循环后面,导致图案右半边凹了进去。记住,空格一定要在星号之前打印,因为输出是"先打印的内容靠左"。3.3 二维列表的遍历与求和图案类的嵌套循环练习的是"输出控制",接下来这个例子更贴近真实业务——处理二维数据。假设你有一个二维列表,存储了三家门店一周七天的销售额,你想算每天全部门店的总销售额,以及每家门店的周总销售额:sales = [ [1200, 980, 1350, 1100, 1500, 1650, 1400], # 门店A [800, 920, 1010, 870, 1100, 1200, 950], # 门店B [1500, 1420, 1280, 1350, 1600, 1750, 1880] # 门店C ] daily_total = [0] * 7 # 每天的全部门店总销售额 store_total = [0] * 3 # 每家门店的周总销售额 for i in range(len(sales)): # i 表示门店 for j in range(len(sales[i])): # j 表示星期几 value = sales[i][j] daily_total[j] += value store_total[i] += value print("每天全部门店总销售额:", daily_total) print("每家门店周总销售额:", store_total)运行结果:每天全部门店总销售额: [3500, 3320, 3640, 3320, 4200, 4600, 4230] 每家门店周总销售额: [9180, 6850, 10780]你看,一次双重循环,同时算出了两组不同的聚合数据。这里的手法很典型:内层循环每次能拿到一个具体元素,根据你"累加到哪个变量"来决定汇总维度。如果内层循环里同时往daily_total[j]和store_total[i]里累加,就相当于一次遍历,两种统计口径都得到了。这个案例我给很多零基础学员演示过,他们看完基本都能秒懂"为什么二维数据要用双重循环"——因为数据本身是二维的,你自然需要用两个下标来定位。这类业务型的例子比打印星号更有"代码感",也更接近真实开发。3.4 用一个批处理场景串联所有知识最后一个案例是一个完整的小项目,用来把前面所有知识串起来。场景是这样的:一家网店要对所有订单做批量结算,每个订单包含若干商品,你想打印出每个订单的金额明细和订单总金额。orders = [ [("键盘", 199), ("鼠标", 89), ("显示器", 1299)], [("U盘", 79), ("硬盘", 499)], [("耳机", 299), ("音箱", 459), ("麦克风", 399), ("声卡", 899)] ] for i, order in enumerate(orders, start=1): total = 0 print(f"订单 {i} 明细:") for name, price in order: print(f" - {name}: {price} 元") total += price print(f" 订单总金额: {total} 元") print("-" * 20)运行结果:订单 1 明细: - 键盘: 199 元 - 鼠标: 89 元 - 显示器: 1299 元 订单总金额: 1587 元 -------------------- 订单 2 明细: - U盘: 79 元 - 硬盘: 499 元 订单总金额: 578 元 -------------------- ...这个案例和前面商场销售的例子异曲同工,都是两层循环遍历"外层列表项、内层明细"。但这里多了一个重要的细节:total变量的初始化位置在外层循环体内、内层循环外。为什么?如果放在内层循环里,每个商品都会把 total 清零一次,最后算出来永远是最后一个商品的价格;如果放在外层循环外,所有订单的金额会混在一起,根本无法区分订单 1、订单 2。判断一个变量该放在哪一层,是嵌套循环里最容易出错的点,也是最值得认真体会的点。我常用的方法是:这个变量是想描述"外层对象的维度"还是"内层对象的维度"?描述订单的,就放在外层循环内;描述商品的,就放在内层循环内。4. 常见问题与排查技巧实录这一节的内容全部来自我实际面对过的报错现场。说真的,循环嵌套的报错几乎都是同样的几个原因反复出现,把它们记住了,排查起来就是分分钟的事。4.1 缩进错乱:逻辑错误的隐形元凶缩进问题有两种表现形式。一种是语法层面的,代码一跑就报IndentationError,这种反而好办,看报错提示的行号就能修。另一种更可怕的,是语法没问题、代码能跑、但结果完全不对——这种大多是因为缩进层级导致代码跑错了位置。举个例子,你想统计"每个子列表中大于 10 的元素个数":data = [[12, 5, 18], [3, 9, 15], [20, 7, 11]] counts = [] for sub in data: count = 0 for num in sub: if num > 10: count += 1 counts.append(count) print(counts) # [2, 1, 2]但如果把counts.append(count)的缩进往右挪一格,变成跟if同级:for sub in data: count = 0 for num in sub: if num > 10: count += 1 counts.append(count) # 错!每次符合条件的都 append 一次结果就会变成[1, 1, 1, 1, 1]这样的鬼东西。同样的代码,一个缩进位置之差,结果天差地别。注意:用 VS Code 或 PyCharm 的同学,强烈建议开启代码缩进可视化(显示空格和 Tab)。缩进错乱导致的 bug 很难直接"看"出来,但可视化之后,错误层级一目了然。4.2 在循环内部修改迭代变量:死循环制造机这是一个非常隐蔽的坑。前面说过,for 循环里用range的时候,修改i的值并不会影响循环的下一次取值,因为 range 已经生成好了一个序列,i每次只是从序列里取下一个值。但如果你在循环体里用了列表或类似结构做迭代对象,同时又修改了那个列表本身,就可能会引发意想不到的问题。至于 dead-loop,最常见的场景其实是 while 和 for 混用的时候。比如这样一段代码:i = 0 while i < 5: for j in range(3): print(i, j) # 忘了写 i += 1 → 死循环这是 while 循环的坑,不是 for 嵌套的坑。for 嵌套里真正常见的是这种:内层循环修改了外层用来控制循环次数的列表。比如:tasks = [1, 2, 3] for task in tasks: for i in range(2): print(task) if task == 2: tasks.append(999) # 迭代时修改了被遍历的列表运行这段代码,可能会卡死,也可能会一直追加下去,行为不可控。Python 的 for 循环是基于索引的,它在遍历的同时修改列表,会引发索引错乱,很多情况下会产生死循环或跳过元素。安全原则:不要在遍历列表的同时修改这个列表。如果确实想过滤或添加元素,建议先复制一份列表再遍历,或者改用 list comprehension 生成新的列表。这条经验从入门到进阶都适用。4.3 break 和 continue 到底作用于哪一层另一个高频困惑是 break 和 continue 的作用范围。很多初学者以为它们能让"最外层循环"也停下来,实际上它们的生效范围是当前所在的这一层循环。也就是说,break 写在哪个 for 里面,它就结束哪个 for。看这个例子:for i in range(3): for j in range(3): if j == 2: break print(i, j)运行结果是:0 0 0 1 1 0 1 1 2 0 2 1注意,每一行打两个数(j=0 和 j=1),到 j=2 时 break,但这里 break 只结束了内层循环。等内层循环 break 之后,代码回到外层循环,i 继续变 1、变 2,外层循环并没有停。如果你确实想在满足某个条件时"跳出一整个双层循环",那就要自己做一个控制标记:found = False for i in range(3): for j in range(3): if i * j == 6: found = True break if found: break print(i, j) # 2 3 吗?不对,条件是 i*j==6,结果是 (2, 3) 不成立,实际是 (2, 3) 才成立?注意 range(3) 只有 0,1,2这里我故意写了个容易混淆的例子,实际range(3)里不含 3,所以i * j == 6永远不会满足。写代码的时候一定要时刻注意 range 的区间。如果真需要跳出多层循环,Python 3.8 之后还可以考虑封装函数配合 return,逻辑会更清晰。这个"跳出多层循环"的需求,在实际业务里并不少见,值得记一下这个控制标记的写法。4.4 常见问题速查表现象可能原因解决方法内层循环一次就结束,外层也跟着结束了break 缩进位置错了,写在了内层外面检查 break 的缩进层级结果比预期少很多内层 range 的起点/终点写错代入几个 i 值验证内层循环次数结果完全重复内层循环没有使用外层变量确认内层逻辑是否需要依赖 i报错 list index out of range用下标遍历时误用了外层长度多用for item in list结构,少用下标数值统计比预期大累加变量初始化放在了外层循环外把累加变量移进外层循环内、内层循环外打印结果没有换行忘记在内层循环外调用print()内层结束后用一个空的print()换行这张表我建议收藏一下,写嵌套循环卡住的时候拿出来对照,比翻文档快得多。5. 循环嵌套的进阶玩法与性能提醒掌握了基础的嵌套循环,你的 Python 之旅已经能覆盖绝大多数二维数据场景了。但想写得又快又好,还有两个进阶点值得关注。5.1 用列表推导式替代简单的双重循环Python 有个特别的优势:很多简单的双层循环可以用列表推导式一行搞定。比如你想把一个二维列表"展平"成一维列表:matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] # 传统双重循环写法 flat = [] for row in matrix: for num in row: flat.append(num) # 列表推导式写法 flat2 = [num for row in matrix for num in row] print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9] print(flat2) # 和 flat 完全一样列表推导式里的for row in matrix for num in row看起来顺序很反直觉,但它确实就是嵌套循环的展开形式——第一个 for 对应外层,第二个 for 对应内层。写熟之后会很爽,但新手阶段我建议先用传统写法理解清楚,再迁移到推导式,否则容易把顺序搞反。5.2 嵌套深度与执行效率的关系前面说过,双层固定循环的总次数等于两层次数相乘。这个乘法增长很快:100 个数量的双层循环是 10000 次,三层就是 1000000 次。在刷题或处理大数据的场景里,把三层循环优化成两层,执行时间可能相差几十倍。举一个常见例子:假设你要在一个列表里找出两数之和等于某个目标值的所有数对。最直接的三层思维是"遍历所有 i、所有 j、再验证一次",但更好的写法是双循环就够,甚至用字典做到单循环。嵌套循环不是越多越好,能省一层就省一层。不过话说回来,对初学者而言,先保证代码正确,再优化效率。很多同学一上来就想着什么优化,结果写出了一堆看不懂的复杂代码,反而更慢。我见过一个学员把一段简单的双层循环改成用一堆位运算,跑是快了 20 毫秒,但代码他自己第二天都看不懂了。适量优化是好事,过度优化就是折腾。5.3 什么时候该把双层循环拆成单层有一类双重循环其实是"伪嵌套"——内外两层循环之间没有逻辑依赖,纯粹是因为数据是二维的。这种时候,Python 的itertools.product可以把嵌套循环扁平化成一个循环:from itertools import product for i in range(3): for j in range(4): print(i, j) # 等价写法 for i, j in product(range(3), range(4)): print(i, j)product(range(3), range(4))会生成 (0,0)、(0,1)、(0,2)、(0,3)、(1,0)……所有组合。它的作用是把两层循环的表达变成一层,逻辑上更线性,读起来更像"遍历一个集合的集合"。这种写法在你需要"嵌套枚举多个维度"时特别有用,比如生成两个列表的笛卡尔积。顺便说一句,product这类工具是 Python 标准库自带的,不需要额外安装任何东西,直接 import 就能用。注意:product适合维度较多、但每层之间无依赖的场景;如果内层循环范围依赖外层变量(比如九九乘法表),那还是老老实实用传统写法,硬套 product 反而更绕。结尾的话:嵌套循环的"肌肉记忆"怎么练最后分享一点我自己的体会。循环嵌套这个东西,本质上不是"会不会写代码"的问题,而是"能不能在脑子里预演循环过程"的问题。你不需要天赋异禀,只需要在初期刻意练习:每看到一个嵌套循环,先在纸上列出前几步的执行痕迹,算出总的执行次数,再看代码的输出是否和预演一致。练上 20 个例子,这个能力就长在你自己身上了。很多人学循环嵌套之所以痛苦,是因为急着背代码、急着跑结果,跳过了"预演"这一步。我对每一个来找我问嵌套循环的初学者都说同一句话:代码跑不出来不是问题,代码跑出来了但你看不懂它为什么这么跑,才是问题。你能把每一步执行顺序讲清楚,嵌套循环你就算真的掌握了。这套方法我已经带过好几波新人验证过,效果很稳定。你也不用一次练完所有案例,挑九九乘法表和订单结算那两个例子先上手,跑通之后再看星号图案和性能优化部分。等你能把一个三层循环的执行顺序在纸上完整写出来的时候,你回头看今天的困惑,就会觉得那只是一个小小的台阶而已。