控制流决定下一条执行哪段代码。Python 用条件、循环、跳转语句与 match 把数据和程序状态映射为执行路径。
条件检查的是对象的真值,而布尔运算符会返回操作数;while 中遗漏状态推进、迭代时修改容器也会悄悄改变路径。
明确写出分支优先级与循环终止条件,优先直接迭代,并用边界值、空输入和每条退出路径测试生成代码。
是什么,为什么存在
控制流(control flow)是程序选择下一条语句的规则。没有控制流,语句只能从上到下执行一次;有了条件、循环与跳转,同一段程序才能针对不同输入选择路径、重复工作或提前结束。
Python 的核心工具各有清楚的职责。if、elif 和 else 选择一个条件分支,for 消费可迭代对象,while 在条件保持为真时重复,而 break、continue 和 return 改变当前结构的正常进程。match 则按数据的形状和值选择分支,而不只是比较一个标量。
你会在输入验证、集合处理、搜索、重试、状态机和请求路由中遇到这些结构。关键不是记住语法,而是能够回答三个问题:条件按什么顺序求值,循环怎样取得下一项,以及哪些路径会离开当前分支、循环或函数。
控制流仍然遵循 Python 的块结构。冒号后的缩进代码属于相应的语句组;缩进变化会改变语义,而不仅是排版。保持每个分支和循环体短小,能让执行路径更容易验证。
工作原理
Python 从当前语句开始,根据条件结果或迭代状态选择后继语句。每个条件表达式都会接受 真值测试(truth value testing) ;它不要求结果必须是字面量 True 或 False。
选择结构时,应先确认控制路径由什么驱动,以及要离开多大范围:
- 一次值选择使用条件表达式,多条语句的互斥选择使用
if链。 - 已有可迭代输入时使用
for,不要手工管理本可省略的索引。 - 重复次数取决于变化状态时使用
while,并写清终止边界。 - 分支取决于嵌套数据的形状和值时考虑
match。 - 需要离开整个函数时使用
return,而不是把break当作跨层跳转。
条件链只选第一条路径
if 先计算自己的条件。若结果为真,就执行对应语句组并跳过同一条链中的其余分支;否则依次检查 elif,最后才可能进入 else。所以,条件顺序就是业务优先级的一部分。
把范围更窄的条件放在范围更宽的条件之前。例如,若先检查 score >= 60,后面的 score >= 90 永远没有机会执行。互斥条件适合写成一条 if 链;相互独立、可能同时成立的条件应写成多个 if。
条件表达式 value_if_true if condition else value_if_false 会先检查 condition,然后只计算两个结果表达式中的一个。它适合简单的值选择,不适合承载多步副作用或多层嵌套。
for 消费迭代器
可迭代对象(iterable) 能提供迭代器,迭代器逐项产出值,直到发出结束信号。for 替你处理取得迭代器、请求下一项和识别结束信号这些步骤,因此遍历集合时通常比手写索引或 while 更直接。
range(stop) 表示从 0 到 stop 之前的整数序列,并不会预先创建一张整数列表。enumerate(iterable, start=1) 同时产出计数和值,dict.items() 则产出键值对。这些工具让循环直接表达需要的数据,而不是通过索引间接查找。
for 的目标可以解包每一项,例如 for key, value in mapping.items()。解包形状必须与每一项兼容,否则循环在该项处抛出 ValueError。循环正常结束后,目标名称仍绑定到最后一项;不要把它误当成只存在于循环内部的块级变量。
while 依赖进度不变量
while 在每轮开始前检查条件。条件为真时执行循环体,然后回到顶部重新检查;一开始就为假时,循环体一次也不执行。它适合迭代次数由状态变化决定的场景,例如有限重试或读取到终止标记。
可靠的 while 循环有一个进度不变量:每条继续执行的路径都会让状态更接近终止。计数器递增、队列缩短或状态转移都可以形成进度。如果某个 continue 绕过了状态更新,循环可能永远停在同一输入上。
跳转语句改变所在结构
几个短语句看起来相近,但影响范围不同:
break立即结束最内层的for或while循环。continue跳过本轮剩余语句,从最内层循环的下一轮开始。return结束整个函数,并把可选结果交还给调用方。pass什么也不做,只在语法要求一条语句时充当空语句。
pass 不会像 continue 那样跳过后续代码。在循环中执行 pass 后,本轮仍会接着运行下一条语句。需要占位实现时可以临时使用它,但发布代码中的空分支通常应说明为何有意忽略该情况。
嵌套循环中的 break 只离开最内层循环。若搜索成功后要离开多层结构,通常把搜索提取成函数并 return,比维护多个标志变量更清楚。
嵌套控制与卫语句
每增加一层嵌套,读者就要同时记住一项条件。 卫语句(guard clause) 先处理无效输入或提前完成的情况并 return 或 continue,可以让主要路径留在较浅的缩进层级。
卫语句不能只是机械地把条件取反。提前退出必须保持原有副作用和清理顺序,并且不能把本应继续处理的项目误写成结束整个函数。改写之后要逐条对照原有路径。
多个独立的 if 不等于一条 if、elif 链。前者可能执行多个语句组,后者最多选择一个;模型为了「减少嵌套」自动互换两者时,常会改变业务语义。
函数退出与异常路径
return 一旦执行,函数中其后的普通语句就不会运行。若循环只是为了寻找一个结果,直接从成功路径返回,并在循环后返回未找到结果,往往比标志变量和循环 else 更容易被团队读懂。
raise 与未处理异常也会转移控制,但目标由异常处理结构决定。finally 中的清理代码会在正常离开、return、break 或异常传播时运行;不要在 finally 中写新的 return 来掩盖原结果或异常。
控制流审查必须包含失败路径。只阅读顺利完成的分支,会遗漏异常发生后资源是否清理、循环是否意外重试,以及调用方能否区分「未找到」与「处理失败」。
循环的 else 表示没有执行 break
Python 的 循环 else(loop else) 在 for 因迭代器耗尽而结束,或 while 因条件变为假而结束时执行。只要同一循环执行了 break,它就不执行。continue 不会禁止 else,因为它只结束当前一轮。
这项语法特别适合搜索:循环找到目标就 break,else 处理搜索完整个输入仍未找到的情况。把它理解为「没有 break 时执行」通常比「循环条件为假时执行」更准确,后者无法解释 for,也容易忽略提前跳出。
match 同时匹配形状和值
结构模式匹配(structural pattern matching) 先计算一次 match 后的主题,再从上到下尝试 case。第一个模式匹配且守卫为真的分支会执行,其余分支被跳过。没有分支匹配时,整个 match 什么也不做,因此通常用 case _ 明确处理默认路径。
模式不是普通布尔表达式。序列模式可以检查长度并解包元素,映射模式可以要求特定键,类模式可以检查类型并取出属性。模式中的捕获名称会绑定值,而不是拿现有局部变量参与相等比较。
模式之后可以加 匹配守卫(match guard) ,写作 case pattern if condition。只有模式先成功,守卫才会求值。守卫适合表达无法由数据形状说明的约束,但它的副作用会让匹配流程难以推理,应保持为简单谓词。
示例
下面四个示例逐步加入分支、循环退出与结构匹配。每段代码都可以独立运行,输出来自本地执行结果。
分类待处理订单
第一个示例直接迭代订单,并用 enumerate() 产生面向读者的序号。未付款订单通过 continue 跳过配送分类;已付款订单只会进入一条按金额排序的条件分支。
orders = [
{"id": "A-104", "paid": True, "total": 135},
{"id": "B-205", "paid": False, "total": 80},
{"id": "C-309", "paid": True, "total": 24},
{"id": "D-410", "paid": True, "total": 72},
]
for position, order in enumerate(orders, start=1):
if not order["paid"]:
print(f'{position}. {order["id"]}: hold')
continue
if order["total"] >= 100:
lane = "priority"
elif order["total"] >= 50:
lane = "standard"
else:
lane = "economy"
print(f'{position}. {order["id"]}: {lane}')1. A-104: priority
2. B-205: hold
3. C-309: economy
4. D-410: standard条件从最高金额向下排列,因此每个已付款订单得到唯一分类。把 total >= 50 放在第一位会错误吞掉部分优先订单,这说明分支顺序本身需要测试。
搜索结束后的 else
这里的 else 与 for 对齐,而不是与 if 对齐。找到商品时,break 同时跳过 else;遍历所有货架仍未找到时,循环才进入 else。
inventory = [
["cable", "mouse"],
["keyboard", "stand"],
["camera", "microphone"],
]
def locate(product):
for shelf, products in enumerate(inventory, start=1):
if product in products:
print(f"{product}: shelf {shelf}")
break
else:
print(f"{product}: unavailable")
locate("camera")
locate("adapter")camera: shelf 3
adapter: unavailable第一次调用在第三个货架执行 break,第二次调用则耗尽迭代器。两条输出分别覆盖循环的提前退出路径与正常完成路径。
有上限的状态轮询
轮询次数由状态决定,所以 while 比遍历业务集合更自然。计数器在任何可能的 continue 或 break 之前递增,因而每轮都向重试上限推进。
poll_results = iter(["pending", "pending", "ready"])
max_attempts = 4
attempt = 0
while attempt < max_attempts:
attempt += 1
result = next(poll_results, "unavailable")
print(f"attempt {attempt}: {result}")
if result == "ready":
print("job completed")
break
else:
print("job did not complete")attempt 1: pending
attempt 2: pending
attempt 3: ready
job completed第三轮取得 ready 后执行 break,因此失败信息不会出现。若四轮都未取得 ready,条件最终变为假,else 才负责报告未完成。
按事件形状路由
最后一个示例把字典键、序列形状、类型检查和守卫放进 case。更具体的非负坐标分支必须排在一般移动分支之前,否则后者会先匹配所有两元素坐标。
def describe_event(event):
match event:
case {"kind": "move", "point": [x, y]} if x >= 0 and y >= 0:
return f"move to ({x}, {y})"
case {"kind": "move", "point": [x, y]}:
return f"move outside grid: ({x}, {y})"
case {"kind": "message", "text": str(text)}:
return f"message: {text}"
case _:
return "unsupported event"
events = [
{"kind": "move", "point": [3, 7]},
{"kind": "move", "point": [-1, 4]},
{"kind": "message", "text": "deploy"},
{"kind": "message", "text": 404},
]
for event in events:
print(describe_event(event))move to (3, 7)
move outside grid: (-1, 4)
message: deploy
unsupported eventstr(text) 是类模式,它要求 text 对应字符串;普通的 text 捕获模式会接受整数 404。默认分支因此能拒绝最后一个形状正确、类型错误的事件。
陷阱
用自然语言方式拼接条件
修复方法: 对每一项写完整比较,如 status == "ready" or status == "queued";需要检查多个允许值时,优先写 status in {"ready", "queued"}。同时测试一个不在集合中的值,避免只用正例掩盖错误。
类似问题也会出现在区间判断中。Python 支持 0 <= percentage <= 100;不要写成 percentage >= 0 or percentage <= 100,因为几乎所有数值至少满足其中一边。
迭代时修改同一个列表
修复方法: 要过滤数据就构造新列表;确实需要原地删除时,可以遍历浅拷贝 items.copy(),或依据问题选择反向索引。不要默认所有容器在迭代期间具有相同修改规则。
字典和集合在迭代期间改变大小通常会抛出 RuntimeError,列表却可能继续运行并给出错误结果。生成代码只通过「没有异常」来判断安全性时,尤其容易漏掉这种静默错误。
while 的某条路径没有推进状态
修复方法: 在循环顶部推进计数,或确保每个回边之前都执行更新。为循环写出进度不变量,并用触发每个 continue 的输入测试它;外部轮询还应有明确上限或截止时间。
while True 并不天然错误,但退出条件必须容易找到并且可达。若循环依赖队列、网络或用户输入,不要让「迟早会返回」代替可验证的终止策略。
把循环 else 当作 if 的备选分支
修复方法: 阅读时把它念成「没有找到就执行」,并确认成功路径确实执行了 break。若控制流仍不直观,把搜索提取成返回结果的函数,调用方再用普通 if 处理结果。
continue 不会跳过循环 else,因为它只开始下一轮。return 或未处理异常会直接离开当前函数或语句组,自然也不会继续执行紧随循环的 else。
在 match 中误用裸名称
修复方法: 字符串、数字和枚举字面量直接写入模式;命名常量使用点号限定名称,例如 case Color.RED:。把具体分支放在一般分支之前,并始终考虑无法识别的输入。
捕获一切的分支会使后续分支不可达,Python 通常会在编译时拒绝明显情况。不过,守卫和复杂模式仍可能让顺序错误变得不明显,因此应为每个分支准备一个输入,再加一个未知输入。
迭代边界与状态所有权
可迭代对象与迭代器不是同一个概念。列表通常能在每次 for 时创建新的迭代器,而迭代器保存当前位置,通常只能向前消费。对同一个已耗尽迭代器再循环,不会自动从头开始。
for 在每轮前向迭代器请求下一项,只有成功取得值才执行循环体。迭代器报告耗尽时,循环正常结束,并可能进入 else。这个结束信号由循环协议处理,不应在普通循环体中手工捕获。
执行 break 不会重置底层迭代器。若其他代码仍持有该迭代器,它可以从剩余位置继续消费;这既能支持分阶段解析,也可能造成调用方意外共享进度。
列表、元组和 range 可以反复迭代,但生成器表达式和生成器对象保存单一消费状态。函数若要多次扫描输入,应声明自己需要可重复迭代的集合,或有意识地只物化一次数据,而不是假定任何 iterable 都能回放。
循环变量在循环后仍然存在,但空迭代不会给它赋值。读取一个只可能在循环体中绑定的名称,会在空输入时触发 NameError 或 UnboundLocalError。把循环后的结果初始化为明确哨兵,或从找到路径直接返回,可以覆盖这项边界。
zip() 默认在最短输入耗尽时结束,因此较长输入的尾部会被忽略。若输入长度必须一致,可在 Python 3.10 及以上使用 zip(..., strict=True),让长度差异抛出 ValueError,而不是静默丢失数据。
迭代状态也有所有权。把同一个迭代器交给两个消费者会让它们交错取走数据;若每个消费者都应看到完整输入,应分别创建迭代器,或传递能创建迭代器的可迭代对象。
真值与短路求值
默认情况下,除非对象定义自己为假,否则它为真。内置假值包括 None、False、数值零,以及空字符串和空容器。用户定义对象可以通过 __bool__() 返回布尔值;没有该方法时,__len__() 返回零也会让对象为假。
因此,if items 适合表达「集合非空」,但它不总能表达领域语义。若 None 表示「未提供」,空列表表示「已提供但没有成员」,写 if value is None 才能保留区别。生成代码常把这两种状态压成同一条假值路径。
and 与 or 先对左操作数做真值测试,并且可能跳过右操作数。left and right 在 left 为假时返回 left,否则返回 right;left or right 在 left 为真时返回 left,否则返回 right。它们返回的是某个操作数,不保证是布尔值。
短路可以安全地保护后续访问,例如 user is not None and user.active 不会在 user 为 None 时读取属性。不要把有副作用的函数调用藏在右操作数中,否则它是否执行取决于左侧数据,控制路径会变得难以察觉。
自定义真值要保持便宜且稳定
__bool__() 应快速返回真正的 bool;返回其他类型会触发 TypeError。__len__() 也不应为了回答是否为空而执行网络请求或消耗迭代器,因为条件可能比调用方预想的更频繁地检查对象。
迭代器本身通常为真,即使它已经耗尽。要判断是否仍有数据,必须请求下一项、使用哨兵,或选择能明确表示长度的数据结构;if iterator 不能替代耗尽检查。
链式比较会让中间表达式只求值一次。lower <= value < upper 同时表达两个边界,并避免把 value 重复计算;它与两个比较用 and 连接的意图一致,但更贴近数学区间。
结构模式匹配的精确语义
match 的主题只计算一次,然后每个 case 按源码顺序尝试。模式成功后才计算守卫;守卫为假时继续尝试后续分支。分支体结束后,控制流离开整个 match,不会像某些语言的 switch 那样自动贯穿到下一分支。
序列模式检查元素结构,但不会把 str、bytes 或 bytearray 当作一般序列拆成字符或字节。映射模式只要求写出的键存在,默认允许额外键;需要检查额外内容时,可以使用 **rest 捕获它们并在守卫中验证。
类模式依赖类型及其模式匹配协议。位置参数由类的 __match_args__ 决定,关键字模式则读取相应属性。对不熟悉的类,关键字写法通常更清楚,也不会假定一个未经核实的位置顺序。
OR 模式写作 pattern_a | pattern_b,两边必须绑定同一组名称,否则分支体无法获得一致的局部环境。_ 是通配符,不创建绑定;其他裸名称通常会捕获主题。
分支顺序与守卫副作用
解释器选择第一个模式成功且守卫为真的分支。因此,具体模式必须先于能覆盖它的一般模式。编译器会拒绝某些明显不可达分支,但无法替你判断业务上是否把高优先级情况放错了位置。
守卫可以调用函数,但应优先使用无副作用的谓词。一个守卫修改状态后返回假,后续分支会看到被修改的状态,执行路径就不再只由原始主题决定。
若多个分支执行几乎相同的动作,只是提取数据的方式不同,可以用 OR 模式合并;若它们代表不同业务优先级,分开书写更便于审查。不要为了使用 match 而把简单的两路布尔判断改成复杂模式。
失败匹配后的名称绑定
模式匹配实现可以在整个模式最终失败前完成部分捕获。规范不保证这些部分绑定会保留还是清除,因此后续代码不能读取失败分支可能捕获的名称。
把捕获名称只用在对应 case 的语句组中,能避开实现差异和陈旧值。若分支需要共同结果,就让每个成功分支显式给同一个结果变量赋值,并提供处理未知输入的默认分支。
4个问题 · 1 道输出预测题 · 1 道找错题