# 控制流

Source: https://codewiki.com/zh/python/control-flow/

> - **what**: 控制流决定下一条执行哪段代码。Python 用条件、循环、跳转语句与 `match` 把数据和程序状态映射为执行路径。
> - **trap**: 条件检查的是对象的真值，而布尔运算符会返回操作数；`while` 中遗漏状态推进、迭代时修改容器也会悄悄改变路径。
> - **fix**: 明确写出分支优先级与循环终止条件，优先直接迭代，并用边界值、空输入和每条退出路径测试生成代码。

## 是什么，为什么存在

控制流（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` 跳过配送分类；已付款订单只会进入一条按金额排序的条件分支。

<!-- quick -->

```python
# file: 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}')
```

```text
1. A-104: priority
2. B-205: hold
3. C-309: economy
4. D-410: standard
```

<!-- /quick -->

条件从最高金额向下排列，因此每个已付款订单得到唯一分类。把 `total >= 50` 放在第一位会错误吞掉部分优先订单，这说明分支顺序本身需要测试。

### 搜索结束后的 `else`

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

```python
# file: 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")
```

```text
camera: shelf 3
adapter: unavailable
```

第一次调用在第三个货架执行 `break`，第二次调用则耗尽迭代器。两条输出分别覆盖循环的提前退出路径与正常完成路径。

### 有上限的状态轮询

轮询次数由状态决定，所以 `while` 比遍历业务集合更自然。计数器在任何可能的 `continue` 或 `break` 之前递增，因而每轮都向重试上限推进。

```python
# file: 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")
```

```text
attempt 1: pending
attempt 2: pending
attempt 3: ready
job completed
```

第三轮取得 `ready` 后执行 `break`，因此失败信息不会出现。若四轮都未取得 `ready`，条件最终变为假，`else` 才负责报告未完成。

### 按事件形状路由

最后一个示例把字典键、序列形状、类型检查和守卫放进 `case`。更具体的非负坐标分支必须排在一般移动分支之前，否则后者会先匹配所有两元素坐标。

```python
# file: 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))
```

```text
move to (3, 7)
move outside grid: (-1, 4)
message: deploy
unsupported event
```

`str(text)` 是类模式，它要求 `text` 对应字符串；普通的 `text` 捕获模式会接受整数 `404`。默认分支因此能拒绝最后一个形状正确、类型错误的事件。

## 陷阱

### 用自然语言方式拼接条件

> **陷阱:** `if status == "ready" or "queued"` 总是通过，因为非空字符串 `"queued"` 本身为真。

**修复方法：** 对每一项写完整比较，如 `status == "ready" or status == "queued"`；需要检查多个允许值时，优先写 `status in {"ready", "queued"}`。同时测试一个不在集合中的值，避免只用正例掩盖错误。

类似问题也会出现在区间判断中。Python 支持 `0 <= percentage <= 100`；不要写成 `percentage >= 0 or percentage <= 100`，因为几乎所有数值至少满足其中一边。

### 迭代时修改同一个列表

> **陷阱:** 在 `for item in items` 内删除 `items` 的元素会移动后续位置，迭代器可能跳过相邻项。

**修复方法：** 要过滤数据就构造新列表；确实需要原地删除时，可以遍历浅拷贝 `items.copy()`，或依据问题选择反向索引。不要默认所有容器在迭代期间具有相同修改规则。

字典和集合在迭代期间改变大小通常会抛出 `RuntimeError`，列表却可能继续运行并给出错误结果。生成代码只通过「没有异常」来判断安全性时，尤其容易漏掉这种静默错误。

### `while` 的某条路径没有推进状态

> **陷阱:** 状态更新写在循环体末尾时，前面的 `continue` 可能绕过更新并造成无限循环。

**修复方法：** 在循环顶部推进计数，或确保每个回边之前都执行更新。为循环写出进度不变量，并用触发每个 `continue` 的输入测试它；外部轮询还应有明确上限或截止时间。

`while True` 并不天然错误，但退出条件必须容易找到并且可达。若循环依赖队列、网络或用户输入，不要让「迟早会返回」代替可验证的终止策略。

### 把循环 `else` 当作 `if` 的备选分支

> **陷阱:** 循环 `else` 取决于是否执行 `break`，不取决于循环体内最后一次 `if` 的结果。

**修复方法：** 阅读时把它念成「没有找到就执行」，并确认成功路径确实执行了 `break`。若控制流仍不直观，把搜索提取成返回结果的函数，调用方再用普通 `if` 处理结果。

`continue` 不会跳过循环 `else`，因为它只开始下一轮。`return` 或未处理异常会直接离开当前函数或语句组，自然也不会继续执行紧随循环的 `else`。

### 在 `match` 中误用裸名称

> **陷阱:** `case RED:` 中的裸名称通常是捕获模式，会匹配任何主题并把主题绑定给 `RED`，而不是与现有变量比较。

**修复方法：** 字符串、数字和枚举字面量直接写入模式；命名常量使用点号限定名称，例如 `case Color.RED:`。把具体分支放在一般分支之前，并始终考虑无法识别的输入。

捕获一切的分支会使后续分支不可达，Python 通常会在编译时拒绝明显情况。不过，守卫和复杂模式仍可能让顺序错误变得不明显，因此应为每个分支准备一个输入，再加一个未知输入。

<!-- deep -->

## 迭代边界与状态所有权

可迭代对象与迭代器不是同一个概念。列表通常能在每次 `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` 的语句组中，能避开实现差异和陈旧值。若分支需要共同结果，就让每个成功分支显式给同一个结果变量赋值，并提供处理未知输入的默认分支。

<!-- /deep -->

[检查点: python/control-flow](https://codewiki.com/zh/python/control-flow/#checkpoint)

## 延伸阅读

- [Python 3.14 语言参考：复合语句](https://docs.python.org/3.14/reference/compound_stmts.html)
- [Python 3.14 标准库参考：真值测试](https://docs.python.org/3.14/library/stdtypes.html#truth-value-testing)
- [Python 3.14 教程：更多控制流工具](https://docs.python.org/3.14/tutorial/controlflow.html)
- [PEP 634：结构模式匹配规范](https://peps.python.org/pep-0634/)
