📖 名词溯源:loop 为什么叫「循环」? 英文 loop 本义是「绳圈、环」。一根绳子首尾相接绕成一个圈,你沿着它走,会一圈一圈回到起点。循环也一样:代码执行到末尾,又「绕回」开头重新执行,周而复始。这个词形象地抓住了「重复 + 绕回」的本质。
🎯 本章学习目标
- 理解为什么「循环」是计算机超越人类重复劳动的关键能力。
- 掌握
while(条件循环)和for(遍历循环)的写法。 - 理解循环的底层原理:它是一次次「跳回去」。
- 理解「循环变量」与「累加」的常见模式。
- 认识死循环,学会用
break和正确的条件避免它。
8.1为什么需要循环:重复是计算机的天赋
想象你要在屏幕上打印「加油」100 次。没有循环,你得把 print("加油") 复制粘贴 100 行。这还只是 100 次,如果是 100 万次呢?你写到手断都写不完。
计算机恰好有一个人类没有的天赋:它能以极快的速度、毫不疲倦地重复做同一件事。人类重复 100 次会烦、会出错,计算机重复 100 万次也面不改色、绝不出错。
而「循环」(loop),就是把这个天赋「打开」的开关——它让一段代码可以反复执行。
# 打印 100 次,只需 3 行
i = 0
while i < 100:
print("加油")
i = i + 1
核心思想 循环的价值在于「用几行代码,驱动几百万次执行」。它把「重复劳动」从人身上转移给了机器。这背后正是计算机最根本的定位:它擅长重复,人类擅长创造。循环,是两者之间的桥梁。
深入一步:循环的三要素
把本章要讲的 while 和 for 抽象一下,会发现一切循环都由三件事构成:初始化(进循环前把状态准备好,比如 i = 1);条件(每圈开始前检查,决定这一圈走不走);更新(每圈结束时让状态朝「终点」挪一步,比如 i = i + 1)。三要素像凳子三条腿,缺一条循环立刻「不完整」:
| 要素 | 它做什么 | 缺了会怎样 |
|---|---|---|
| 初始化 | 准备好条件里要用的变量 | 变量不存在,直接 NameError |
| 条件 | 每圈开始前判断「还继续吗」 | 没有可依据的判断,循环无从谈起 |
| 更新 | 让状态朝条件变假的方向移动 | 条件永远不变——零圈或死循环 |
以后每写完一个循环,用三要素从头到尾过一遍,是自查的最快方法——尤其「更新」那条:问自己「哪个变量、朝哪个方向、每圈挪多少」,答得上来,循环就基本是对的。
📐 例题 1 起飞前逐件检查
起飞前要对 5 个部件逐一做目视检查。用 for 循环打印每个部件的检查记录,格式为「部件 n 检查完成」(n 为 1 到 5,各占一行),全部检查完后打印「全部部件检查完毕」。
思路 处理对象明确是「1 到 5 的每个整数」——个数固定、逐个处理,正是 for 的主场。range(1, 6) 含头不含尾,恰好给出 1、2、3、4、5 五个数。循环体只做一件事:打印当前部件的记录;收尾那句「全部完毕」跟具体某个部件无关,写在循环外。
解
for i in range(1, 6):
print("部件", i, "检查完成")
print("全部部件检查完毕")
验证 机器实测输出 6 行:部件 1 到部件 5 的检查记录各一行,外加收尾一行「全部部件检查完毕」——收尾只打印一次,证明它确实在循环外。
答:代码如上,5 件检查 + 1 行收尾。⚠ 易错点:一是把 range(1, 5) 当成 1 到 5——它只含 1、2、3、4 四个数,少检一个部件;二是把最后一句缩进进循环体——「全部完毕」会连打 5 次,收尾变成了复读机。for 的循环体到哪行结束,看缩进。
工程视角 真实检查单动辄几十上百项,但循环写起来一模一样——把 range(1, 6) 的 6 改成 101,就从 5 件检查变成 100 件。循环让程序能处理的事务规模,第一次脱离了代码的行数:写 5 件的程序和写 500 件的程序,循环部分只有终值不同。这就是「重复」被机器接管后的世界。
辨析:循环是「时间的重复」,函数是「空间的重复」
同一件事要做 100 遍(每遍用不同数据)——用循环;同一段逻辑要在 100 个地方出现——用函数(第 9 章)。循环消灭「重复的行」,函数消灭「重复的逻辑」,两者互为镜像。判断口诀:数一数「重复发生在时间轴上还是代码轴上」——时间轴交给循环,代码轴交给函数。
再进半步:三要素在 for 里是「隐形」的
对照看 for 版的部件检查:for i in range(1, 6): 一行同时声明了三要素——range 的起点 1 是初始化,「数到 6 之前」是条件,步长 1 是更新。三要素被压缩进一行,由解释器替你严格执行,所以 for 天生比 while 难写出死循环——更新永远自动发生。能数着次数完成的重复,优先写 for;状态复杂、条件刁钻才请 while 出山,这是两条路线的实用分工。反过来说,while 给你「手动挡」的全部自由,也就要求你亲手负责三要素的每一条——自由与责任,从来是一对。
8.2while:条件为真就一直重复
第一种循环叫 while 循环,它的结构是:
while 条件:
要重复的代码 # 缩进
它的规则是:只要「条件」还是 True,就一遍遍执行缩进的代码;每执行完一遍,回头重新检查条件;直到条件变成 False 才停止。
看一个完整例子,并逐行解释:
count = 1
while count <= 3:
print(count)
count = count + 1
count = 1:初始化为 1。- 检查
count <= 3:1 ≤ 3 为真,进入循环体,打印 1。 count = count + 1:count 变成 2。- 回到条件检查:2 ≤ 3 为真,打印 2,count 变 3。
- 再检查:3 ≤ 3 为真,打印 3,count 变 4。
- 再检查:4 ≤ 3 为假,循环结束。
最终输出 1、2、3。
要点 while 循环最关键的,是循环体里必须让条件最终能变成 False。上面的 count = count + 1 就是干这个的——它一点点「逼近」退出条件。少了这行,循环就永远停不下来。
深入一步:先判断,后执行——「零圈循环」是合法的
while 的执行顺序永远是先看条件,再决定走不走:如果条件在第一次判断时就是假,循环体一次都不会执行——这叫零圈循环。它不是 bug,而是语义本身:while 表达的是「只要还满足就继续做」,本来就不保证至少做一次。比如初始余额 250 元、条件「低于 200 才继续存」,第一圈判断就不通过,循环体一圈不转——完全正常。有些语言另提供 do…while(先做一圈再判断,保证至少执行一次),Python 没有这个语法;真需要「至少一次」时,惯用做法是先做一遍,再用 while 续。初学阶段的习惯应当是:写 while 之前,先在脑中跑一次第一次判断——入口状态想清楚,循环就稳了一半。
📐 例题 2 存款几年翻倍
一笔 100 元的存款,年利率 3%,按复利计息(每年的利息并入本金)。问第几年余额突破 200 元?用 while 模拟:每年更新一次余额,突破后打印「第 n 年突破 200 元,此时有 x 元」(余额保留两位小数)。
思路 「要循环几年」事先不知道——次数不固定,这是 while 的典型信号。三要素:初始化 money = 100.0、year = 0;条件 money < 200(还没翻倍就继续);更新 year = year + 1、money = money * 1.03(一年过去,本金涨 3%)。打印放在循环结束后。
解
money = 100.0
year = 0
while money < 200:
year = year + 1
money = money * 1.03
print("第", year, "年突破 200 元,此时有", round(money, 2), "元")
验证 机器实测:第 22、23、24 年末的余额分别为 191.61、197.36、203.28 元——第 23 年末还差一点,第 24 年末成功越线,程序输出「第 24 年突破 200 元,此时有 203.28 元」。
答:第 24 年,余额约 203.28 元。⚠ 易错点:一是两行更新写反或漏掉 year 更新——year 不涨,输出年份就永远错;二是把 round 提前塞进计算——round() 只该管显示,计算全程用原值,每圈都四舍五入会把误差滚雪球式放大(本题恰好不受影响,但这是侥幸不是正确)。
工程视角 「等一个条件达成」是 while 的另一大主场:等温度降到阈值以下、等油压稳定在区间内、等传感器读数连续正常——这些场景的共同点是次数不可预知、条件说了算。把「等到什么时候」写成条件,把「等的时候做什么」写成循环体,while 的选型就完成了。
辨析:零圈是设计,不是事故
很多人第一次遇到「循环体一圈没执行」会怀疑自己写错了。反过来想:初始余额若是 250 元(已超过目标),循环零圈、直接打印「第 0 年」——程序依然正确,因为「已经达标、无事可做」正是正确的回答。默认接受零圈,代码反而简单;真有「至少要做一次」的业务要求,再加手段不迟。把「零圈也算对」想通,while 的语义就完整了。
再进半步:条件的边界符号,又回来对质了
本题条件是 money < 200——「低于 200 才继续存」。若业务口径改成「达到 200 就停」,条件该写 < 200 还是 <= 200?区别只在「恰好等于 200」那一刻是否再存一年:本例的余额序列恰好没有踩在 200.00 上(23 年末 197.36、24 年末 203.28),两种写法结果相同;但换一组利率,边界差异就会真实显现。循环条件和分支条件一样,每个边界符号都要拿回业务原文对质——「超过」「不低于」「至少」这些词在第 7 章怎么抠,这里就怎么抠。
8.3for:依次遍历一组东西
第二种循环叫 for 循环。它最常用的场景是:「有一组东西(比如一串数字、一个列表),我想一个一个拿出来处理」。
for i in range(1, 4):
print(i)
这里 range(1, 4) 会依次产生 1、2、3(注意:不含 4)。for 循环的意思是:让 i 依次等于 1、2、3,每取一个值就执行一次循环体。输出同样是 1、2、3。
while 和 for 的区别在于「想问题的角度」:
| 维度 | while | for |
|---|---|---|
| 思路 | 「只要条件满足就重复」 | 「把一组东西逐个取出来」 |
| 何时用 | 不知道要重复多少次(如「直到用户输入 quit」) | 知道要遍历多少个(如「处理 1 到 100」) |
| 循环次数 | 由条件决定,可动态变化 | 由集合大小决定,较固定 |
补充一个关键细节:range(a, b) 是「前闭后开」——包含 a,不包含 b。range(1, 4) 是 1、2、3,共 3 个数。为什么这么设计?因为这样 range(0, n) 正好产生 n 个数(0 到 n-1),配合后面要学的「列表下标从 0 开始」最自然。
🧠 一句话记忆 while 靠「条件」循环,for 靠「遍历」循环。次数不固定用 while,要逐个处理用 for。
深入一步:range 的三种形态
| 写法 | 含义 | 给出的数 |
|---|---|---|
range(n) | 从 0 数到 n−1 | range(4) → 0, 1, 2, 3 |
range(a, b) | 从 a 数到 b−1(含头不含尾) | range(1, 6) → 1, 2, 3, 4, 5 |
range(a, b, step) | 从 a 出发,每步跨 step | range(5, 0, -1) → 5, 4, 3, 2, 1 |
三个细节值得点破。其一,含头不含尾:终值永远取不到——这样 range(1, 6) 恰好 5 个数,数个数时「大头减小头」直接心算。其二,step 可以为负:从大到小倒数,前提是方向一致——起点 5、终点 0、步长 −1,一路递减;若方向矛盾(如 range(5, 0) 默认步长 +1),得到的是空数列,循环零圈。其三,range 是个「按需生产的数列发生器」:要用一个给一个,从不一口气造出全部数字——所以 range(0, 1000000) 也不占大内存。for 是 Python 里唯一「天然配循环」的关键字,它不挑食:凡是能挨个交出元素的东西都能遍历——字符串(第 5 章的逐字符)、文件(第 11 章的逐行)都在其列,range 只是其中最常用的「数字队」。
📐 例题 3 发射倒计时与奇数队
(1)发射前 5 秒倒计时:打印 5、4、3、2、1 各占一行,随后打印「点火!」。(2)另写一段循环,逐行打印 1 到 9 之间的所有奇数。
思路 倒数要从大到小——负步长登场:起点 5,终点 0(不含),步长 −1。奇数队从 1 出发、步长 2,自然全是奇数;终点 10 不含,最后一个数是 9。「点火!」跟秒数无关,写在循环外。
解
for t in range(5, 0, -1):
print(t)
print("点火!")
for n in range(1, 10, 2):
print(n)
验证 机器实测:第一段依次输出 5、4、3、2、1,然后是「点火!」;第二段依次输出 1、3、5、7、9,共 5 行——起点 1,每步跨 2,恰好停在 9。
答:代码如上。⚠ 易错点:倒计时的终点写 0(不含 0,数到 1 为止);若写成 range(5) 则变成 0 到 4 正着数——倒计时变正计时,方向全反。step 的符号必须与「起点到终点」的方向一致,方向矛盾得到空数列。
嵌套循环:外层走一步,内层走一圈
循环体里再放一个循环,就是嵌套循环。它只有一条铁律:外层每前进一轮,内层都要从头到尾完整走一遍。想象外层是「天」、内层是「小时」:过完一天(外层一轮),当天的小时(内层)是从 1 重新数到 24 的。总执行次数 = 外层次数 × 内层次数。
📐 例题 4 乘法表前三行
用嵌套 for 循环打印乘法表前 3 行,每行的格式为若干个「j×i=积」依次排列、式子之间用一个空格隔开。例如第 2 行输出「1×2=2 2×2=4」。并回答:内层循环体一共执行了多少次?
思路 行号 i 从 1 到 3,交给外层;第 i 行恰好有 i 个式子,所以内层 j 从 1 到 i。一行内的多个式子要打在同一行——用变量 line 在内层里逐个「累积」字符串,内层结束后统一打印。line 在外层每轮开头新建,天然避免上一行残留。
解
for i in range(1, 4):
line = ""
for j in range(1, i + 1):
line = line + str(j) + "×" + str(i) + "=" + str(i * j) + " "
print(line)
验证 机器实测输出三行:1×1=1、1×2=2 2×2=4、1×3=3 2×3=6 3×3=9。内层执行次数:外层 3 轮,各轮内层分别走 1、2、3 次——共 1+2+3 = 6 次,恰等于 3 行式子的总个数。
答:代码如上,内层共执行 6 次。⚠ 易错点:一是 print(line) 缩进层级——缩进进内层,一行会拆成多次打印;二是 line 忘了清空——上一行的式子会「传染」到下一行。数字转字符串要用 str() 拼接——这正是第 5 章学的招。
工程视角 100 个部件 × 8 个检查项目 = 800 次内层执行——嵌套循环的工作量按乘法放大。写嵌套之前先心算总量级:外层几千、内层几百的「两小段代码」,实际可能跑上百万圈。嵌套一时爽,规模火葬场,动手前先数零。
辨析:range 不是「存着数的容器」
print(range(1, 6)) 打印的是 range(1, 6),不是数字——因为 range 对象不是「装着 1 到 5 的盒子」,而是「如何生产这些数的说明书」。for 拿着说明书边读边生产,要一个给一个。想看它产什么,老老实实用 for 遍历打印。真正「装着数的盒子」叫列表,第 10 章才登场;现在只需记住:range 配 for,天经地义。
再进半步:循环变量的命名讲究
例题里的 i、j、k、n 不是随手拈来——i、j 作循环变量是从早期编程语言沿袭至今的行业惯例,看到它们,全世界程序员都秒懂「这是个纯计数器」。但惯例只适用于纯计数:循环变量本身没有业务含义时,用 i、j 简短明了;一旦变量承载业务含义——部件号、行号、年份——就该换成有含义的名字:for part in range(1, 6)、for row in range(1, 4)。命名的一贯原则在循环里同样成立:名字替代码说话,读代码的人(包括三个月后的你)少猜一层。嵌套循环习惯上外层 i、内层 j,一层一个字母,与数学里的下标记号一脉相承。
8.4底层原理:循环就是「跳回去」
上一章我们说「分支 = 跳过去」,这一章是它的镜像:循环 = 跳回去。
在硬件层面,一个 while 循环翻译成机器码,大概是这样的结构:
指令1: 比较 count 和 3 # 判断条件
指令2: 如果 count > 3,跳到 指令5 # 条件不满足,跳出循环
指令3: 打印 count # 循环体
指令4: count = count + 1 # 更新
指令5: 跳回 指令1 # ← 关键:跳回开头!
看到没?最后一行的「跳回指令1」就是循环的灵魂——它是一条 无条件跳转指令(JMP),把程序计数器 PC 往回改,让 CPU 重新执行开头的判断。
底层原理 分支是「往前跳」(跳过一段),循环是「往后跳」(回到前面一段)。两者用的都是同一种硬件机制——改写程序计数器。整个程序的控制流,本质就是「顺序执行 + 各种跳转」的排列组合。
🧠 一句话记忆 if = 跳过去,while/for = 跳回去。跳转,就是改程序计数器。
深入一步:循环的机器级解剖
把 while 放到机器视角下,整个循环只有两条跳转:一条条件跳转——判断为假时跳出循环;一条无条件跳转——循环体走完后跳回判断处。对照成五个零件:A 初始化(在循环之外);B 每圈开头做条件比较(点标志位);C 条件为假 → 条件跳转,整个循环结束;D 条件为真 → 执行循环体(含更新);E 循体末尾无条件跳转,跳回 B。第 7 章说过「分支向前跳」;循环的全部秘密,就是多了一条往回跳的指令——同一套跳转机制,方向一反,就凭空获得了「重复」的能力。
📐 例题 5 手工逐圈追踪:数到 3
读下面的程序,先不运行,逐圈写出每次判断时的 n 值、判断结果与本圈动作;再回答:循环体共执行几圈?条件共判断了几次?
n = 0
while n < 3:
n = n + 1
print("第", n, "圈")
print("退出时 n =", n)
思路 用「判断—动作」表逐圈追踪:每次判断记下 n 的当前值与比较结果,通过才执行这一圈。
| 判断次序 | 此时 n | n < 3 ? | 动作 |
|---|---|---|---|
| 第 1 次 | 0 | 真 | 进圈:n 变 1,打印「第 1 圈」,跳回 |
| 第 2 次 | 1 | 真 | 进圈:n 变 2,打印「第 2 圈」,跳回 |
| 第 3 次 | 2 | 真 | 进圈:n 变 3,打印「第 3 圈」,跳回 |
| 第 4 次 | 3 | 假 | 跳出循环,执行收尾打印 |
验证 机器实测输出 4 行:第 1 圈、第 2 圈、第 3 圈、「退出时 n = 3」——与手工追踪逐行一致。
答:循环体执行 3 圈,条件判断 4 次。判断不通过的那一次,不算一圈——「圈数」数的是循环体执行次数,比判断次数少 1(首次判断就失败则是零圈)。⚠ 易错点:认为「循环结束」也要再执行一圈收尾——退出时 n 是 3 不是 4,因为 n 变成 3 之后程序再也没有进过循环体。
辨析:分支与循环,在机器层合流
现在把两章连起来看:分支是「比较 + 向前跳转」(跳过一段代码),循环是「比较 + 向回跳转」(跳回判断处)。两者用的是同一套标志位与跳转指令——第 7 章埋下的伏笔在此兑现:结构化定理说的「三种结构」,在机器层面真的只靠两种方向的跳转就拼出来了。你写的每一行 if 和 while,最终都汇入这条同源的大河。
再进半步:老牌语言的 for 长什么样
顺带开个眼界:C 语言的传统写法是 for (i = 0; i < n; i = i + 1)——三要素被显式写在括号里,用分号隔开,跟 while 的关系几乎是「抄写格式不同」。Python 的 for 走了另一条路:括号里只剩「从哪儿逐一取元素」,三要素整体被 range 吸收。设计哲学的差异很值得玩味:C 认为「数数」是循环的本职,Python 认为「挨个处理一堆东西」才是循环的本职——这也是为什么 Python 的 for 能直接遍历字符串、文件,而不用先数长度再数下标。
8.5循环变量与累加:最常见的模式
循环最常见的用途之一是累加——把一堆数加起来。看这个经典例子:求 1 加到 100 的和。
total = 0
for i in range(1, 101):
total = total + i
print(total) # 5050
这个模式有个专门的名字,叫 累加器(accumulator)。它的套路是:
- 循环前,准备一个变量
total = 0(初始为 0)。 - 循环里,每次都把当前值「加进」total:
total = total + i。 - 循环结束后,total 就是所有数的和。
注意 total = total + i 这一行的深意:等号右边用「旧的 total」,算出新值,再存回 total。这不是数学方程,而是「读旧值 → 加 → 写回」的赋值过程。整个过程 total 的值像滚雪球一样越滚越大。
🧠 一句话记忆 累加三件套:初始化 0 → 循环里 total = total + i → 循环后 total 就是总和。
深入一步:循环不变式——每一圈都为真的那句话
判断循环写得对不对,计算机科学里有一件利器:循环不变式(loop invariant)——一句「每圈开始时都为真」的断言。以累加 1 到 100 为例(total = 0; n = 1; while n <= 100: total = total + n; n = n + 1),不变式是:第 k 圈开始时,total 恰好等于 1+2+…+(k−1),且 n 恰好等于 k。它是否可信,看三步:
- 初始成立:第 1 圈开始前 total = 0(前 0 个数之和),n = 1——断言成立。
- 每圈保持:若某圈开始时断言成立,这一圈执行 total 加 n(把第 k 个数收进来)、n 增 1——下一圈开始时断言依然成立。
- 结束时得结论:条件 n ≤ 100 变假即 n = 101 时循环停,此时 total = 1+2+…+100 = 5050(机器实测高斯求和恰为 5050)。
这三步,就是数学归纳法的程序版:初始 = 基底,保持 = 归纳步,结束 = 取结论。不变式不是玄学,是「为什么这个循环算出来的就是对的」的完整论证。
📐 例题 6 累乘器:5 的阶乘
阶乘 5! = 1×2×3×4×5。用累乘器模式计算:变量 product 从 1 出发,for 循环把 1 到 5 逐个乘进去。要求打印每一圈后的 product,并写出本题的循环不变式。
思路 累乘器与累加器同骨架:求和用 total = total + n(初值 0,加法单位元),累乘用 product = product * k(初值 1,乘法单位元——写 0 的话乘谁都是 0)。不变式:第 k 圈开始时 product = (k−1)!。
解
product = 1
for k in range(1, 6):
product = product * k
print("乘到", k, "时,product =", product)
print("5 的阶乘 =", product)
验证 机器实测 6 行输出:product 依次为 1、2、6、24、120,最后一行「5 的阶乘 = 120」——每一圈的值都符合「(k−1)! 乘进 k 得 k!」的不变式。
答:5! = 120;不变式「第 k 圈开始时 product = (k−1)!」。⚠ 易错点:初值写成 0——0 乘任何数都是 0,结果永远是 0。「求和初值 0,累乘初值 1」这对口诀值得刻进肌肉记忆:初值必须是该运算的单位元。
📐 例题 7 辗转相除:最大公约数
求 48 和 18 的最大公约数。辗转相除法(欧几里得算法):用大数除以小数取余数,然后「小数、余数」接替「大数、小数」继续,直到余数为 0——此时的除数就是答案。要求每圈打印 a、b、余数 r 的值。
思路 三要素齐了:a = 48、b = 18 初始化;条件 b != 0(余数还没到 0);更新 a = b; b = r(小数升格为被除数,余数升格为除数)。它的不变式很有味道:每圈前后 gcd(a, b) 不变——「大数除以小数的余数」不改变两数的最大公约数,这正是欧几里得算法两千年前就证好的数学心脏。
解
a = 48
b = 18
while b != 0:
r = a % b
print("a =", a, "b =", b, "余数 r =", r)
a = b
b = r
print("最大公约数 =", a)
验证 机器实测 4 行:a = 48 b = 18 余数 12;a = 18 b = 12 余数 6;a = 12 b = 6 余数 0;最大公约数 = 6。数列 (48,18) → (18,12) → (12,6) → (6,0),每一跳都满足「余数接替」,gcd 始终是 6。
答:gcd(48, 18) = 6。⚠ 易错点:a = b 和 b = r 的先后——若先写 b = r 再写 a = b,r 还没被用过就被 b 的旧值覆盖规则搞乱,两行全错。多变量「同时换岗」要么严格按「先用后存」的顺序分行写,要么用元组赋值 a, b = b, a % b 一句完成(第 10 章展开)。
工程应用与辨析:累加器家族
total(求和,初值 0)、product(累乘,初值 1)、count(计数,初值 0)、best(当前最大,初值取第一个元素——写 0 的话,全负数序列会翻车)……它们共用同一副循环骨架,只换「更新那一行」——这一族模式统称累加器模式(accumulator pattern)。你已经在用的内置函数 sum(),本质就是别人替你写好的求和循环——读懂这章,等于读懂半个标准库。不变式还能当调试利器:在循环体开头加一句 print,把「不变式该成立的量」每圈打印出来对拍——哪一圈数字开始不对,bug 就埋在哪一圈。这比盯着代码发呆快得多,值得养成习惯。
再进半步:把不变式写进注释
工程里的成熟做法,是把不变式作为一行注释放在循环正上方:# 不变式:每圈开始时 total == 前 k-1 项之和。这行注释有两个消费者:一是未来的你——重读代码时先看不变式,循环的意图立刻复活;二是替你评审代码的人——拿着注释逐句对代码,对不上就是 bug 或注释过期。注释写「为什么对」,代码写「怎么做」,两者配合才完整。面试和代码评审里,「说不出自己循环的不变式」几乎是硬伤——从本章开始练,正当时。
8.6死循环与 break:如何停下来
循环有个危险的坑:死循环(infinite loop)——条件永远为真,循环永远停不下来。
count = 1
while count <= 3:
print(count)
# 忘了写 count = count + 1
上面这行代码,count 永远是 1,条件 1 <= 3 永远为真,于是程序会无限打印 1,永不停止,直到你强制中断它。
⚠️ 如何识别和避免死循环 死循环的根本原因是「让条件变假的那一步」缺失或写错。写 while 循环时,务必检查:循环体里有没有能改变条件变量的语句(如 count = count + 1)。如果程序卡住、风扇狂转、很久没反应,很可能就是死循环。
break:主动退出
有时你想「在满足某个条件时,立刻退出循环」。这时用 break:
for i in range(1, 100):
if i > 5:
break # i 超过 5 就立刻跳出循环
print(i)
这段代码只打印 1 到 5。当 i = 6 时,i > 5 为真,执行 break,整个循环立即结束。
| 语句 | 作用 |
|---|---|
break | 立即跳出整个循环 |
continue | 跳过本轮剩余代码,进入下一轮 |
🧠 一句话记忆 break = 彻底跳出循环;continue = 跳过本轮,继续下一轮。死循环 = 忘了让条件变假。
深入一步:死循环的三副面孔
- 忘了更新:
i = 0之后再没人动过 i,while i < 3永远为真——上节小测验里正是这副面孔。 - 更新方向反了:条件指望 n 涨到 100 才停,循环体里却写着
n = n - 1——n 离终点越来越远,条件永远为真。 - 没有出口的 while True:条件恒真,循环体里又没有 break——进得来,出不去。
三副面孔,病灶相同:循环的世界里没有「走累了就停」——必须有一个变量朝着「条件变假」的方向持续移动,或者有一条设计好的 break 通道。自查时按三要素逐条过:更新变量是谁?方向对吗?出口在哪?三个问题答完,八成死循环当场现形。
「卡住」不一定是死循环
程序跑了很久,不等于死循环——判断标准不是「等了多久」,而是「状态有没有朝终点移动」。最快的检查法:在循环体开头加一句进度打印(当前处理到第几项 / 总共多少项),跑起来盯着数字:数字在涨 → 只是任务大,等它(或回头优化算法);数字纹丝不动 → 死循环,中断找病灶。一行 print 换来一眼判断,是初学阶段最划算的一笔投资。
服务循环:while True 不是罪
真实系统的主程序,往往就是一个 while True:服务器等着接下一个请求、监控程序每秒读一次传感器、游戏每一帧刷新一次画面——「永远转」正是需求本身。它们不死的秘密在出口设计:收到关闭指令就 break、超时计数满就退出。死循环事故与服务循环的区别,不在 while True 这行代码,而在有没有设计好的出口。初学阶段建议先练熟带明确条件的循环;while True 等第一版跑通、确认需要一个「永不主动结束」的骨架时再引入,是更稳的路线。
📐 例题 8 有限次数重试:门锁密码
门锁密码是 4752,最多允许尝试 3 次。模拟「前两次都输错、第三次输对」的过程:每次尝试打印结果与剩余机会;结束后打印累计尝试次数,并给出最终状态(门锁已打开 / 账户已锁定)。
思路 这是「次数上限 + 达成即停」的双重出口:条件写 tries < 3 and not ok——次数没耗尽、且还没成功,才继续。ok 是标志位(第 7 章的思想):密码对上就置 True,下一圈判断直接短路退出。模拟三次输入用「前两次错、第三次对」的分支写死,实际程序里这一步会换成读输入。
解
tries = 0
ok = False
while tries < 3 and not ok:
tries = tries + 1
if tries < 3:
guess = 1111 # 模拟:前两次都输错
else:
guess = 4752 # 第三次输对
if guess == 4752:
ok = True
print("第", tries, "次尝试:成功")
else:
print("第", tries, "次尝试:失败,还剩", 3 - tries, "次机会")
print("累计尝试", tries, "次")
if ok:
print("门锁已打开")
else:
print("三次失败,账户已锁定")
验证 机器实测 5 行:前两次各打印「失败,还剩 2/1 次机会」,第三次「成功」,随后「累计尝试 3 次」「门锁已打开」——成功后循环立即结束,不会把第三次机会浪费掉。
答:代码如上,三次尝试后成功开门。⚠ 易错点:条件只写 tries < 3——若密码第二次就对了,程序还会多试一次(虽然结果碰巧不变,但「多余的一试」在真实系统里可能就是一次锁定)。凡「达成目标就该停」的循环,都需要一个成功标志位参与条件——这正是「设计出口」的具象化。
📐 例题 9 continue:只跳过本轮
求 1 到 10 中所有不是 3 的倍数的整数之和。然后只把 continue 换成 break(其余一字不动),观察结果变成多少,并解释差异。
思路 「不是 3 的倍数」翻译成 n % 3 == 0 的否定——满足它就 continue:跳过本轮剩余语句(其实只剩累加那一句),直接进入下一轮。换成 break,则 n = 3 时整个循环立刻终止。
解
total = 0
for n in range(1, 11):
if n % 3 == 0:
continue
total = total + n
print("1 到 10 中非 3 的倍数之和 =", total)
total = 0
for n in range(1, 11):
if n % 3 == 0:
break
total = total + n
print("换成 break 后 total =", total)
验证 机器实测:continue 版输出 37(总和 55 减去 3 的倍数 3+6+9 = 18,55−18 = 37);break 版输出 3(n = 1、n = 2 各累加一次,n = 3 时循环整体结束,4 到 10 根本没轮到)。
答:continue 版 37,break 版 3。⚠ 易错点:把两者都理解成「跳过」——continue 跳的是一圈(本轮剩余部分),break 断的是整个循环。换一个记法:continue 是「这单不做了,接下一单」;break 是「今天收工」。
工程视角 重试、轮询、巡检,都是「while True + 设计出口」的主力场景:重试有次数上限,轮询有超时,巡检有停止指令——出口写在设计里,而不是出事后再想。反过来,排查事故死循环的三板斧也在这里:找更新变量、验更新方向、找出口,三问三答。
辨析:break 与 continue 的「方向感」
把两个关键字的动作用方向感记牢:continue 向上——回到下一轮判断,跳过的只是本轮剩余;break 向外——离开整个循环,后面的圈全部取消。再补一条重要边界:两者都只作用于最近一层循环——嵌套循环里,内层的 break 只跳出内层,外层照常转。想「里层出事、外层也停」,需要第 7 章的标志位思想:内层发现异常先置标志、break 出内层,外层每圈检查标志、发现即 break——一圈一层,逐层上报,像极了真实组织里的逐级上报机制。
再进半步:真实世界的重试,还讲究「退避」
例题 8 的重试是「立刻再试」——三次尝试间隔为零。真实系统不敢这么干:密码锁可以,但「请求失败后立刻重试」会在服务端故障时雪上加霜——大家挤在同一个瞬间反复冲击,服务恢复的第一秒就被打趴。所以工程上的重试普遍带退避(backoff):失败后等 1 秒再试,再失败等 2 秒、4 秒……间隔逐次拉长,给对端喘息的时间。用本章的知识其实已能模拟:循环里加一个 wait = 1,每圈失败后 wait = wait * 2——指数就藏在「每圈乘 2」这行更新里。真正的「等 1 秒」需要时间工具,第 11 章见。
本章小结
- 循环让计算机不知疲倦地重复,用几行代码驱动几百万次执行。
while靠条件循环(条件真就重复),for靠遍历循环(逐个取元素)。- 循环的底层原理是「跳回去」——用无条件跳转指令把程序计数器改回开头。
- 累加器是最常见模式:初始化 → 循环里累加 → 循环后得总和。
- 死循环是忘了让条件变假;
break可主动跳出,continue跳过本轮。 - 嵌套循环「外层走一步、内层走一圈」,总执行次数 = 外层次数 × 内层次数;动手前先心算总量级。
- 循环不变式是自查利器:初始、保持、结束三步论证(归纳法的程序版);调试时每圈打印对拍,哪圈断就查哪圈。
读完本章,你应该能……
- 用 while 打印 1 到 5,并说清每一轮循环变量的变化。
- 用 for 和 range 实现同样的效果。
- 写出求 1 到 100 之和的累加器程序。
- 用自己的话解释「循环 = 跳回去」的底层原理。
- 识别一个死循环,并说清它的原因和解决办法。
- 用嵌套循环打印乘法表前 3 行,并说出内层循环体共执行多少次(答案:1+2+3 = 6 次)。
- 用辗转相除法求 gcd(252, 105),逐圈写出 a、b、r 的值(答案:最大公约数 21)。
思考题
1. 用循环求 1 到 100 之间所有偶数的和,怎么写?
提示:可以用 range(2, 101, 2)(步长为 2),或在循环里用 if i % 2 == 0 过滤。
2. range(1, 5) 会产生哪几个数?为什么不含 5?
提示:1、2、3、4。range 是「前闭后开」,含起点不含终点。
3. 用自己的话解释:while 循环在硬件上是如何「往复执行」的?
提示:末尾一条无条件跳转指令把程序计数器 PC 改回开头,反复执行;直到条件跳转指令把 PC 改到循环外。
4. 下面的代码会怎样?为什么?
i = 0; while i < 3: print(i)
提示:死循环。因为循环体里没有改变 i 的语句,i 永远是 0,条件永远为真。
5. 外层 for i in range(1, 4) 嵌套内层 for j in range(1, 5),内层循环体一共执行多少次?
提示:外层 3 轮,每轮内层都完整走 4 次——3 × 4 = 12 次(机器实测 12)。数嵌套循环的次数,先数外层次数、再数内层次数,相乘即得。
6. 例题 9 中只把 continue 换成 break,total 变成多少?为什么?
提示:变成 3。n = 1、n = 2 各累加一次后,n = 3 时 break 直接结束整个循环,4 到 10 根本没轮到(机器实测 3)。continue 结束的是「一圈」,break 结束的是「整个循环」。
7. 服务循环和事故死循环可能都在「一直跑」,怎么区分?
提示:看两处——一是有没有设计好的出口(break 条件、次数上限、超时、停止信号);二是状态是否朝终点移动(加进度打印对拍,数字在涨就只是任务大,纹丝不动才是死循环)。