AI 技术博客
返回首页
Python 基础 · 15 分钟阅读

while 循环:条件驱动与无限循环的控制

## 引言 前两篇的分支语句让程序「转弯」,本篇让程序「反复走同一段路」——这是流程控制里最后一块核心拼图:循环(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. 对条件做一次真值测试; 2. 为真 → 执行整个块,然后**跳回第 1 步**重新测试; 3. 为假 → 跳过块,继续执行循环后面的代码。 关键差异在语气上:`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——正好用来挡掉负数与非数字。 ## 操作与实现 先看最小的条件循环——倒数发射,注意循环体如何改变条件: ```python 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 的和算出来——先初始化「累计变量」再循环: ```python total = 0 # 累计器:必须初始化 n = 1 while n <= 100: total += n n += 1 print(total) # 5050 ``` `5050` 这个数字值得记住:1 到 100 求和本是等差数列(101 × 50),循环以笨办法验证同一答案——程序不讲究聪明,讲究正确。累计器 `total` 若忘记初始化,循环体第一行 `total += n` 就会 `NameError`(陷阱四)。 哨兵值退出:处理「任务队列」,空串是结束信号: ```python tasks = ["登录", "拉取数据", "清洗", ""] # 空串 = 哨兵:队列结束 index = 0 task = tasks[index] while task: # 空串为假,直接退出 print(f"处理任务:{task}") index += 1 task = tasks[index] # 更新哨兵:读到空串即停 print("任务队列处理完毕") # 任务队列处理完毕 ``` 这个例子把三个要点压在一起:真值名单(空串为假)、**循环体必须更新测试所依赖的变量**(`task` 每轮换代)、哨兵设计(用数据自身标记终点,不用布尔标志)。 标志位模式:模拟「重试直到连接成功」——这是网络程序的原型: ```python 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` 的初次见面: ```python 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 篇细讲): ```python count = 0 while True: print("运行中…", count) count += 1 if count >= 3: break # 立即结束循环;第 5 篇系统讲解 print("已退出") # 已退出 ``` `while True` 代表「无条件持续」,`break` 是它的刹车——「死循环不是错的,没有刹车的死循环才是错的」是读这段代码该记住的一句话。 最后是循环输入校验的经典骨架(标志位版,本机交互运行): ```python 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`): ```text 输入 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` 更优雅的答案。 ## 练习与思考题 1. 不运行代码,写出下面程序的输出,再运行验证:`n = 1`,`while n < 10: print(n)`,`n = n * 2`——它是死循环还是正常结束?把 `10` 改成 `16` 呢? 2. 用标志位模式写「猜数字」的骨架:`secret = 7`,程序反复读整数(用 `input()`),猜中才退出——并用本篇的 `isdigit()` 双层校验处理非法输入。(猜数字完整版是第 8 篇的实战主题。) 3. 找出下面代码的 bug 并用三种不同方式修正:`i = 0`,`total = 0`,`while i < 5: total += i`——提示:缺了什么?修正后 `total` 应等于多少? 4. 思考题:`while` 的条件能接受哪些「假值」?在没有 `break` 的情况下,分别用「标志位」与「哨兵值」重写第 3 题,说明两者各自适合什么场景?(提示:重温第 2 章真值测试名单。)