引言
前两篇的分支语句让程序「转弯」,本篇让程序「反复走同一段路」——这是流程控制里最后一块核心拼图:循环(loop)。如果说 if 回答「要不要做」,那 while 回答「做到什么时候」。人做重复的事情会疲惫、会出错,机器不会:逐行处理日志、等待任务完成、重试直到成功,这些「不知道做几次、只知道何时停」的重复,正是 while 的主场。
本篇是第三章第三篇,阅读前提:掌握第一篇的 if 语法(while 的块结构与它完全一致)、第 2 章的真值测试规则(while 的条件同样只问「真还是假」)。范围划清:for 循环与 range 是第 4 篇的主题;break / continue 是第 5 篇的主题——本篇涉及它们时仅一句带过并注明,绝不抢先展开;while 与 else 的组合本篇点到为止,时机成熟在第 5 篇收官。
概念与原理
为什么需要循环:三种基本结构之一
软件界有一个 1968 年确立的基本共识(Dijkstra 在《Go To Statement Considered Harmful》中系统阐发):任意程序逻辑都可以只用三种控制结构拼装——顺序(一行接一行)、分支(if 家族)、循环(while/for)。if 我们已经会用,「代码复用」又属于函数(第 4 章),而循环是唯一的「让同一段代码按条件反复执行」的本土结构。没有它,处理 100 行日志意味着把同一段代码复制 100 遍;有了它,代码写一遍、执行一万次。
while 与 for 的分工一句话:while 擅长「不知道多少次、只知道什么条件停」(重试到成功、轮询到可用、消费到队列空);for 擅长「知道要遍历什么」的确定性迭代(第 4 篇)。本篇先建立 while 的完整心智。
while 的执行流程:三拍节奏
while 的语法与 if 同构:while 条件: 加缩进块。执行流程是三拍的循环:
- 对条件做一次真值测试;
- 为真 → 执行整个块,然后跳回第 1 步重新测试;
- 为假 → 跳过块,继续执行循环后面的代码。
关键差异在语气上:if 是「测一次、做一次」,while 是「测一次、做一次,做完再测一次,直到测出假」。因此块内必须存在改变条件的东西——变量更新、输入、或者显式退出——否则第 1 步永远为真,循环变成死循环(详见下文)。真值测试的名单与 if 完全同一套:None、False、数值零、空容器为假,其余为真,所以 while queue: 表示「队列非空就一直处理」,这是真实工程里最常见的写法。
死循环:成因、代价与退出
条件恒为真的循环即死循环(infinite loop):while True: 是最直白的写法,块内没有任何改变条件的语句同样构成死循环。运行死循环的代价是真实的:程序占满一个 CPU 核心空转,界面「卡死」,必须强制中断——交互式环境里按 Ctrl+C 会抛出 KeyboardInterrupt 异常(异常机制第 7 章展开)终止程序,编辑器与终端也有停止按钮。while True 并不是禁忌:菜单、事件监听、连接保持等服务型代码刻意用它(条件永远为真,靠内部显式退出),但每次写 while True 都必须同时想清楚退出路径——这是工程纪律,不是风格偏好。
退出策略主要有三:其一,break 语句立即结束循环(第 5 篇细讲,本文先认识它);其二,标志位(flag):用布尔变量记录「是否该停」,循环条件测试它;其三,让被测试的条件本身发生变化——循环体内更新参与条件的变量,最终把它推出真值名单。后两种是本篇主角,因为它们是「条件驱动」的字面含义,也最不容易写坏。
标志位模式与哨兵值
标志位模式(flag pattern):先定义一个布尔变量 done = False,循环条件是 while not done:,循环体内的某处分支在合适的时机把 done 置为 True。它把「什么时候停」这个决策从循环条件里挪进循环体——条件处只需要读懂一个词「没完就继续」。网络重试、等待条件满足、多级确认退出,这类「不知道第几次尝试会成功」的场景是它的主战场;相比直接在循环条件里写「读输入 → 判断」的长表达式,标志位把判断逻辑留在块内、条件永远是一行 while not done:,可读性显著占优。
哨兵值(sentinel)是标志位的变体:用一个特殊值(而非专用布尔)表示「结束」。最经典的是空字符串:while task: 处理一个任务列表,task 被更新为 "" 时条件为假、循环自然结束。空值即假恰好落进真值名单,不需要任何额外比较。
while 与 else:正常结束才执行的收尾
while 可以带 else 子句:while 条件: 块 else: 收尾块——只有循环「自然结束」(条件变假)时才执行 else 块;如果循环被 break 提前终止,则跳过它。语义总结一句话:「循环完整跑完,才做收尾」。它最适合「确保遍历完整」的检查型代码(比如扫描完之后确认没漏网)。本篇只到这一层,break 与 else 的完整配合在第 5 篇。
循环输入校验:程序的看门人
真实程序的数据大多来自外部——用户键盘、配置文件、网络报文,一律不可全信。循环输入校验(input validation loop)是 while 最现实的工程场景:反复读取输入,非法就提示重试,直到拿到一个合法值再继续。经典骨架是 while True + 读取 + 校验 + break;但为了本篇不抢第 5 篇的戏,我们先用标志位版本走通逻辑——你会发现两者只是「退出路径放哪儿」的区别。校验手法上我们首次使用字符串方法 isdigit():它判断一个字符串是否全部由数字字符组成(空串返回 False),"88".isdigit() 为 True、"-1".isdigit() 为 False——正好用来挡掉负数与非数字。
操作与实现
先看最小的条件循环——倒数发射,注意循环体如何改变条件:
count = 5
while count > 0: # 条件:还有剩余
print(count) # 5 4 3 2 1
count -= 1 # 关键:更新条件变量,否则死循环
print("发射!") # 发射!
count -= 1 是循环的灵魂所在:每轮把 count 减一,5、4、3、2、1 各打印一次后 count 变为 0,条件 0 > 0 为假,循环结束,打印「发射!」。把它注释掉重跑——count 永远是 5,条件恒真,这就是死循环的现场(运行 5 秒 CPU 转满,然后 Ctrl+C 收场)。
累加器模式:把 1 到 100 的和算出来——先初始化「累计变量」再循环:
total = 0 # 累计器:必须初始化
n = 1
while n <= 100:
total += n
n += 1
print(total) # 5050
5050 这个数字值得记住:1 到 100 求和本是等差数列(101 × 50),循环以笨办法验证同一答案——程序不讲究聪明,讲究正确。累计器 total 若忘记初始化,循环体第一行 total += n 就会 NameError(陷阱四)。
哨兵值退出:处理「任务队列」,空串是结束信号:
tasks = ["登录", "拉取数据", "清洗", ""] # 空串 = 哨兵:队列结束
index = 0
task = tasks[index]
while task: # 空串为假,直接退出
print(f"处理任务:{task}")
index += 1
task = tasks[index] # 更新哨兵:读到空串即停
print("任务队列处理完毕") # 任务队列处理完毕
这个例子把三个要点压在一起:真值名单(空串为假)、循环体必须更新测试所依赖的变量(task 每轮换代)、哨兵设计(用数据自身标记终点,不用布尔标志)。
标志位模式:模拟「重试直到连接成功」——这是网络程序的原型:
attempts = [False, False, True] # 模拟三次探测的结果
i = 0
connected = False
while not connected:
print(f"第 {i+1} 次探测…")
connected = attempts[i] # 成功时刻把标志位翻真
i += 1
print("连接成功") # 连接成功
while not connected: 是标准的标志位形状:条件里只有标志变量,翻真的动作深藏在块内。换成真实网络场景,attempts[i] 就是「发起请求并判断响应」的代码——循环结构一字不改。
while 与 else 的初次见面:
n = 5
total = 0
while n > 0:
total += n
n -= 1
else:
print("循环自然结束") # 循环自然结束
print(f"1+...+5 = {total}") # 1+...+5 = 15
else 块在条件变假后执行(本例必然执行)。它真正的用武之地要等第 5 篇的 break 登场:break 提前离开时 else 会被跳过——「完整跑完才收尾」的语义到那时才有完全体,现在先记住「自然结束 → 执行 else」。
while True 与显式退出(break 仅此一见,第 5 篇细讲):
count = 0
while True:
print("运行中…", count)
count += 1
if count >= 3:
break # 立即结束循环;第 5 篇系统讲解
print("已退出") # 已退出
while True 代表「无条件持续」,break 是它的刹车——「死循环不是错的,没有刹车的死循环才是错的」是读这段代码该记住的一句话。
最后是循环输入校验的经典骨架(标志位版,本机交互运行):
score = -1
valid = False
while not valid:
raw = input("输入 0-100 的分数: ")
if raw.isdigit() and 0 <= int(raw) <= 100:
score = int(raw)
valid = True # 校验通过,翻真退出
else:
print("分数无效,请重试")
print(f"收到分数:{score}")
isdigit() 先行挡掉非数字(连负数也被挡),再叠加范围判断 0 <= int(raw) <= 100——第一道门管「是不是数字」,第二道门管「数字在不在范围」。真实运行记录(Python 3.12.10,输入依次为 150、abc、88):
输入 0-100 的分数: 150
分数无效,请重试
输入 0-100 的分数: abc
分数无效,请重试
输入 0-100 的分数: 88
收到分数:88
150 是数字但超界、abc 非数字、88 双层校验全过——两条无效路径都进了重试分支,程序从头到尾没有崩溃。把 valid = True 与 break 互换,就是「流派二」的写法,两种骨架在工程里都常见,第 5 篇会对比它们的取舍。
易错点与陷阱
陷阱一:忘记更新循环变量——死循环的头号成因
while count > 0: 块内没有 count -= 1,或更新语句被缩进错误地移出循环体(多缩进/少缩进),条件恒真,程序卡死。这是教科书第一坑,真实工程里等价形态是「忘记推进游标/忘记消费队列/忘记重赋值」。排查口令:写完 while 先找「哪条语句让条件往假的方向走」,找不到就该补;运行中卡死,先按 Ctrl+C 看栈回溯停在循环哪一行(KeyboardInterrupt 的栈信息会指到正在执行的循环体),再检查变量的更新路径。
陷阱二:边界差一——多跑一轮与少跑一轮
while n > 0 与 while n >= 0 差一个单位:前者在 1 时执行最后一次、0 时停;后者会多执行一轮 0。倒计时、分页、累加这类「数着做」的循环最容易栽在这一个符号上。做法:先在纸面上写出退出时条件变量的值(本例是 0),再倒推每一轮打印的值,确认首尾都在预期内;或者干脆用第 4 篇的 for + range,边界写成参数更不易错。
陷阱三:while True 没有刹车
菜单、监听、常驻服务都需要 while True,但「写着写着忘了退出路径」就会让用户面对一个关不掉的程序。规则三条:while True 的块内必须有 break / 标志位 / 异常退出之一;所有分支都要覆盖退出时机(比如 if 命中之外的分支也得想清楚);服务型循环至少保证 Ctrl+C 可中断(不要在例外处理里吞掉 KeyboardInterrupt,第 7 章细讲)。
陷阱四:循环里的变量没初始化
累计器 total、计数器 i、标志位 connected 在循环条件或循环体里被使用前必须初始化。漏了初始化:参与运算的变量直接 NameError(total += n 触发「在赋值前引用」);而只读不写的变量(如 while queue: 里潜在的 queue 未定义)报 NameError 更早。奇怪的是标志位忘初始化往往不报错——while not connected: 若在块内首次赋值,Python 把它当作循环体的局部变量,条件处读取时它还未绑定,照样 NameError。初始化变量是循环的前置仪式,别省略。
小结
while 是「测试 → 执行 → 再测试」的条件循环:条件为真就再来一遍,条件变假才放行;条件接受一切真值,空容器与零天然是「停止信号」。循环体必须让条件最终变假,三件退出武器——break(第 5 篇细讲)、标志位、哨兵值——各有分工,while True 必须配刹车。else 子句只在循环自然结束时执行。循环输入校验用双层门(isdigit() 挡非数字、范围判断挡越界)把非法输入拦在门外。四大陷阱:忘更新变量、差一错误、while True 无刹车、变量未初始化。下一篇进入 for 与 range:当「要做多少次」确定或「要遍历什么」明确时,for 是比 while 更优雅的答案。
练习与思考题
- 不运行代码,写出下面程序的输出,再运行验证:
n = 1,while n < 10: print(n),n = n * 2——它是死循环还是正常结束?把10改成16呢? - 用标志位模式写「猜数字」的骨架:
secret = 7,程序反复读整数(用input()),猜中才退出——并用本篇的isdigit()双层校验处理非法输入。(猜数字完整版是第 8 篇的实战主题。) - 找出下面代码的 bug 并用三种不同方式修正:
i = 0,total = 0,while i < 5: total += i——提示:缺了什么?修正后total应等于多少? - 思考题:
while的条件能接受哪些「假值」?在没有break的情况下,分别用「标志位」与「哨兵值」重写第 3 题,说明两者各自适合什么场景?(提示:重温第 2 章真值测试名单。)