控制流

用条件分支、迭代、循环控制和结构模式匹配准确表达执行路径,并避开无限循环、错误真值测试与 match 捕获陷阱。

难度 入门 时长 标准深度约 14分钟
版本 Python 3.14
what

控制流决定下一条执行哪段代码。Python 用条件、循环、跳转语句与 match 把数据和程序状态映射为执行路径。

trap

条件检查的是对象的真值,而布尔运算符会返回操作数;while 中遗漏状态推进、迭代时修改容器也会悄悄改变路径。

fix

明确写出分支优先级与循环终止条件,优先直接迭代,并用边界值、空输入和每条退出路径测试生成代码。

是什么,为什么存在

控制流(control flow)是程序选择下一条语句的规则。没有控制流,语句只能从上到下执行一次;有了条件、循环与跳转,同一段程序才能针对不同输入选择路径、重复工作或提前结束。

Python 的核心工具各有清楚的职责。ifelifelse 选择一个条件分支,for 消费可迭代对象,while 在条件保持为真时重复,而 breakcontinuereturn 改变当前结构的正常进程。match 则按数据的形状和值选择分支,而不只是比较一个标量。

你会在输入验证、集合处理、搜索、重试、状态机和请求路由中遇到这些结构。关键不是记住语法,而是能够回答三个问题:条件按什么顺序求值,循环怎样取得下一项,以及哪些路径会离开当前分支、循环或函数。

控制流仍然遵循 Python 的块结构。冒号后的缩进代码属于相应的语句组;缩进变化会改变语义,而不仅是排版。保持每个分支和循环体短小,能让执行路径更容易验证。

工作原理

Python 从当前语句开始,根据条件结果或迭代状态选择后继语句。每个条件表达式都会接受 真值测试(truth value testing) ;它不要求结果必须是字面量 TrueFalse

选择结构时,应先确认控制路径由什么驱动,以及要离开多大范围:

  • 一次值选择使用条件表达式,多条语句的互斥选择使用 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) 表示从 0stop 之前的整数序列,并不会预先创建一张整数列表。enumerate(iterable, start=1) 同时产出计数和值,dict.items() 则产出键值对。这些工具让循环直接表达需要的数据,而不是通过索引间接查找。

for 的目标可以解包每一项,例如 for key, value in mapping.items()。解包形状必须与每一项兼容,否则循环在该项处抛出 ValueError。循环正常结束后,目标名称仍绑定到最后一项;不要把它误当成只存在于循环内部的块级变量。

while 依赖进度不变量

while 在每轮开始前检查条件。条件为真时执行循环体,然后回到顶部重新检查;一开始就为假时,循环体一次也不执行。它适合迭代次数由状态变化决定的场景,例如有限重试或读取到终止标记。

可靠的 while 循环有一个进度不变量:每条继续执行的路径都会让状态更接近终止。计数器递增、队列缩短或状态转移都可以形成进度。如果某个 continue 绕过了状态更新,循环可能永远停在同一输入上。

跳转语句改变所在结构

几个短语句看起来相近,但影响范围不同:

  • break 立即结束最内层的 forwhile 循环。
  • continue 跳过本轮剩余语句,从最内层循环的下一轮开始。
  • return 结束整个函数,并把可选结果交还给调用方。
  • pass 什么也不做,只在语法要求一条语句时充当空语句。

pass 不会像 continue 那样跳过后续代码。在循环中执行 pass 后,本轮仍会接着运行下一条语句。需要占位实现时可以临时使用它,但发布代码中的空分支通常应说明为何有意忽略该情况。

嵌套循环中的 break 只离开最内层循环。若搜索成功后要离开多层结构,通常把搜索提取成函数并 return,比维护多个标志变量更清楚。

嵌套控制与卫语句

每增加一层嵌套,读者就要同时记住一项条件。 卫语句(guard clause) 先处理无效输入或提前完成的情况并 returncontinue,可以让主要路径留在较浅的缩进层级。

卫语句不能只是机械地把条件取反。提前退出必须保持原有副作用和清理顺序,并且不能把本应继续处理的项目误写成结束整个函数。改写之后要逐条对照原有路径。

多个独立的 if 不等于一条 ifelif 链。前者可能执行多个语句组,后者最多选择一个;模型为了「减少嵌套」自动互换两者时,常会改变业务语义。

函数退出与异常路径

return 一旦执行,函数中其后的普通语句就不会运行。若循环只是为了寻找一个结果,直接从成功路径返回,并在循环后返回未找到结果,往往比标志变量和循环 else 更容易被团队读懂。

raise 与未处理异常也会转移控制,但目标由异常处理结构决定。finally 中的清理代码会在正常离开、returnbreak 或异常传播时运行;不要在 finally 中写新的 return 来掩盖原结果或异常。

控制流审查必须包含失败路径。只阅读顺利完成的分支,会遗漏异常发生后资源是否清理、循环是否意外重试,以及调用方能否区分「未找到」与「处理失败」。

循环的 else 表示没有执行 break

Python 的 循环 else(loop else) for 因迭代器耗尽而结束,或 while 因条件变为假而结束时执行。只要同一循环执行了 break,它就不执行。continue 不会禁止 else,因为它只结束当前一轮。

这项语法特别适合搜索:循环找到目标就 breakelse 处理搜索完整个输入仍未找到的情况。把它理解为「没有 break 时执行」通常比「循环条件为假时执行」更准确,后者无法解释 for,也容易忽略提前跳出。

match 同时匹配形状和值

结构模式匹配(structural pattern matching) 先计算一次 match 后的主题,再从上到下尝试 case。第一个模式匹配且守卫为真的分支会执行,其余分支被跳过。没有分支匹配时,整个 match 什么也不做,因此通常用 case _ 明确处理默认路径。

模式不是普通布尔表达式。序列模式可以检查长度并解包元素,映射模式可以要求特定键,类模式可以检查类型并取出属性。模式中的捕获名称会绑定值,而不是拿现有局部变量参与相等比较。

模式之后可以加 匹配守卫(match guard) ,写作 case pattern if condition。只有模式先成功,守卫才会求值。守卫适合表达无法由数据形状说明的约束,但它的副作用会让匹配流程难以推理,应保持为简单谓词。

示例

下面四个示例逐步加入分支、循环退出与结构匹配。每段代码都可以独立运行,输出来自本地执行结果。

分类待处理订单

第一个示例直接迭代订单,并用 enumerate() 产生面向读者的序号。未付款订单通过 continue 跳过配送分类;已付款订单只会进入一条按金额排序的条件分支。

route_orders.py
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

这里的 elsefor 对齐,而不是与 if 对齐。找到商品时,break 同时跳过 else;遍历所有货架仍未找到时,循环才进入 else

find_product.py
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 比遍历业务集合更自然。计数器在任何可能的 continuebreak 之前递增,因而每轮都向重试上限推进。

poll_job.py
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。更具体的非负坐标分支必须排在一般移动分支之前,否则后者会先匹配所有两元素坐标。

route_events.py
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 event

str(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 都能回放。

循环变量在循环后仍然存在,但空迭代不会给它赋值。读取一个只可能在循环体中绑定的名称,会在空输入时触发 NameErrorUnboundLocalError。把循环后的结果初始化为明确哨兵,或从找到路径直接返回,可以覆盖这项边界。

zip() 默认在最短输入耗尽时结束,因此较长输入的尾部会被忽略。若输入长度必须一致,可在 Python 3.10 及以上使用 zip(..., strict=True),让长度差异抛出 ValueError,而不是静默丢失数据。

迭代状态也有所有权。把同一个迭代器交给两个消费者会让它们交错取走数据;若每个消费者都应看到完整输入,应分别创建迭代器,或传递能创建迭代器的可迭代对象。

真值与短路求值

默认情况下,除非对象定义自己为假,否则它为真。内置假值包括 NoneFalse、数值零,以及空字符串和空容器。用户定义对象可以通过 __bool__() 返回布尔值;没有该方法时,__len__() 返回零也会让对象为假。

因此,if items 适合表达「集合非空」,但它不总能表达领域语义。若 None 表示「未提供」,空列表表示「已提供但没有成员」,写 if value is None 才能保留区别。生成代码常把这两种状态压成同一条假值路径。

andor 先对左操作数做真值测试,并且可能跳过右操作数。left and rightleft 为假时返回 left,否则返回 rightleft or rightleft 为真时返回 left,否则返回 right。它们返回的是某个操作数,不保证是布尔值。

短路可以安全地保护后续访问,例如 user is not None and user.active 不会在 userNone 时读取属性。不要把有副作用的函数调用藏在右操作数中,否则它是否执行取决于左侧数据,控制路径会变得难以察觉。

自定义真值要保持便宜且稳定

__bool__() 应快速返回真正的 bool;返回其他类型会触发 TypeError__len__() 也不应为了回答是否为空而执行网络请求或消耗迭代器,因为条件可能比调用方预想的更频繁地检查对象。

迭代器本身通常为真,即使它已经耗尽。要判断是否仍有数据,必须请求下一项、使用哨兵,或选择能明确表示长度的数据结构;if iterator 不能替代耗尽检查。

链式比较会让中间表达式只求值一次。lower <= value < upper 同时表达两个边界,并避免把 value 重复计算;它与两个比较用 and 连接的意图一致,但更贴近数学区间。

结构模式匹配的精确语义

match 的主题只计算一次,然后每个 case 按源码顺序尝试。模式成功后才计算守卫;守卫为假时继续尝试后续分支。分支体结束后,控制流离开整个 match,不会像某些语言的 switch 那样自动贯穿到下一分支。

序列模式检查元素结构,但不会把 strbytesbytearray 当作一般序列拆成字符或字节。映射模式只要求写出的键存在,默认允许额外键;需要检查额外内容时,可以使用 **rest 捕获它们并在守卫中验证。

类模式依赖类型及其模式匹配协议。位置参数由类的 __match_args__ 决定,关键字模式则读取相应属性。对不熟悉的类,关键字写法通常更清楚,也不会假定一个未经核实的位置顺序。

OR 模式写作 pattern_a | pattern_b,两边必须绑定同一组名称,否则分支体无法获得一致的局部环境。_ 是通配符,不创建绑定;其他裸名称通常会捕获主题。

分支顺序与守卫副作用

解释器选择第一个模式成功且守卫为真的分支。因此,具体模式必须先于能覆盖它的一般模式。编译器会拒绝某些明显不可达分支,但无法替你判断业务上是否把高优先级情况放错了位置。

守卫可以调用函数,但应优先使用无副作用的谓词。一个守卫修改状态后返回假,后续分支会看到被修改的状态,执行路径就不再只由原始主题决定。

若多个分支执行几乎相同的动作,只是提取数据的方式不同,可以用 OR 模式合并;若它们代表不同业务优先级,分开书写更便于审查。不要为了使用 match 而把简单的两路布尔判断改成复杂模式。

失败匹配后的名称绑定

模式匹配实现可以在整个模式最终失败前完成部分捕获。规范不保证这些部分绑定会保留还是清除,因此后续代码不能读取失败分支可能捕获的名称。

把捕获名称只用在对应 case 的语句组中,能避开实现差异和陈旧值。若分支需要共同结果,就让每个成功分支显式给同一个结果变量赋值,并提供处理未知输入的默认分支。

延伸阅读

检查点

4个问题 · 1 道输出预测题 · 1 道找错题

复制为 Markdown 面试题库 在 GitHub 上编辑 报告错误 讲清楚了吗?