# 列表推导式

Source: https://codewiki.com/zh/python/list-comprehensions/

> - **what**: 列表推导式（list comprehension）按顺序遍历可迭代对象，对通过筛选的元素求值，并立即生成一个新列表。
> - **trap**: 末尾的 `if` 负责丢弃元素，开头的 `X if condition else Y` 负责选择结果；混淆两者会造成语法错误或错误数据。
> - **fix**: 子句较少、每个输入最多产生一个结果时使用推导式；需要副作用、复杂分支或逐步调试时改用普通循环。

## 是什么，为什么存在

列表推导式是一种列表显示形式。
它把「遍历输入、可选筛选、计算输出、追加到新列表」写成一个表达式，基本形式是 `[expression for item in iterable if condition]`。
无论输入是不是列表，结果都是一个新的列表对象。

这里的输入只需是可迭代对象（iterable），例如列表、元组、`range`、字典视图或生成器。
每个通过条件的元素都会让开头的表达式求值一次，结果按遍历顺序放入列表。
没有通过条件的元素不会进入结果，也不会求值开头的表达式。

它解决的是简单映射和筛选代码过于分散的问题。
普通循环需要先创建空列表，再写 `for`、条件和 `append()`；推导式把同一条数据规则放在一处。
阅读时仍应能快速回答两个问题：输入来自哪里，什么值会进入结果。

你会在数据清洗、API 响应整形、测试夹具和参数组合中遇到它。
只要任务自然地表达为「从每个输入得到零个或一个输出」，推导式通常很合适。
如果一次输入可能追加多个不同结果，或循环体还要更新外部状态，普通循环往往更直白。

列表推导式只负责构造列表。
字典推导式、集合推导式和生成器表达式沿用相似的子句结构，但它们的结果类型、重复值处理和求值时机不同。
把这些形式都叫作「列表推导式」会掩盖实际行为差异。

## 工作原理

最左侧是结果表达式，之后至少有一个 `for` 子句，还可以跟零个或多个 `for` 或 `if` 子句。
执行顺序并不是从最左侧表达式开始，而是按照 `for` 和 `if` 从左到右展开。
只有执行路径到达最内层时，结果表达式才会求值并追加一次。

下面两段逻辑等价。推导式中的子句顺序，与把它展开成语句后从外层到内层的顺序相同：

`[transform(item) for item in source if keep(item)]`

对应的循环先遍历，再筛选，最后转换并追加：

`for item in source` → `if keep(item)` → `result.append(transform(item))`

这个顺序会影响正确性。
筛选条件可以阻止结果表达式运行，所以危险或昂贵的转换可以只对有效输入执行。
反过来，如果筛选和结果表达式都调用同一个函数，该函数会对保留的元素执行两次；它还可能有状态，从而让两次结果不一致。

多个 `for` 子句等价于嵌套循环。
`[pair for left in lefts for right in rights]` 先固定一个 `left`，再完整遍历 `rights`，随后才进入下一个 `left`。
后面的子句可以使用前面子句绑定的名称，调换顺序则可能改变结果顺序，甚至让名称尚未定义。

推导式末尾的 `if` 是过滤子句，没有 `else`。条件表达式放在结果位置，形式是 `when_true if condition else when_false`，因此可以让每个输入产生不同形态的输出。这两种 `if` 可以同时出现：先由末尾条件决定是否保留，再由开头条件决定保留后产生什么。

列表推导式会立即消费输入并构造完整结果。它适合之后需要索引、重复遍历或修改的结果。如果数据只会交给 `sum()`、`any()`、`all()` 等消费一次，生成器表达式通常能避免先构造中间列表。

### 从 `for` 子句向外阅读

面对多行推导式时，不要按视觉顺序从结果表达式猜执行过程。先找到第一个 `for`，再把后续子句按出现顺序缩进展开，最后把最左侧表达式看成最内层的 `append()` 参数。这个方法同时适用于列表、集合和字典推导式。

可以按下面的顺序检查一段推导式：

1. 找到最左侧 `for` 后的输入，确认它能提供哪些值。
2. 记录迭代目标的解包形状，例如 `for key, value in mapping.items()`。
3. 从左到右应用后续 `for` 和 `if`，确认每个名称已经绑定。
4. 计算结果表达式，并确认它对每条到达此处的路径只运行一次。
5. 根据括号判断结果容器，再检查调用方是否依赖顺序、重复值或重复消费。

在基本形式中，各部分的职责固定：

| 部分 | 示例 | 职责 |
| --- | --- | --- |
| 结果表达式 | `normalize(item)` | 创建要追加的值 |
| 迭代子句 | `for item in source` | 依次绑定输入值 |
| 过滤子句 | `if item.active` | 决定本轮是否到达结果表达式 |
| 条件表达式 | `x if condition else y` | 为已保留输入选择结果 |

语法虽然允许多个连续的 `if`，但它们只是逐层筛选。`[x for x in values if ready(x) if valid(x)]` 与用 `and` 连接两个条件的保留规则相同，并按从左到右短路。若第二个条件依赖第一个条件已经排除某类值，拆成两个子句有时更容易看清这个前提。

### 用结果基数选择结构

一个输入恰好产生一个输出时，只需要转换表达式和一个 `for`。一个输入产生零个或一个输出时，再添加过滤子句。一个输入需要产生多个输出时，可以增加后续 `for`，但只有当嵌套关系本身简单而稳定时才值得压缩。

如果循环需要在不同分支追加不同数量的结果，列表推导式会变得别扭。分组、累计、提前退出和逐项容错都需要语句级控制流，这些任务不应硬塞进一个表达式。判断标准不是最终能否写成推导式，而是展开后是否仍是一条清楚的数据路径。

## 示例

下面四个示例逐步加入筛选、条件表达式、多个迭代子句和赋值表达式。每段程序都可以独立运行，输出来自本地 `python3` 执行结果。

### 转换并筛选

付款状态是筛选规则，收据文字是转换规则。未付款订单不会执行格式化表达式，也不会出现在新列表中。

<!-- quick -->

```python
# file: paid_receipts.py
orders = [
    {"id": "A100", "total": 82.5, "paid": True},
    {"id": "A101", "total": 19.0, "paid": False},
    {"id": "A102", "total": 120.0, "paid": True},
]

receipts = [
    f'{order["id"]}: ${order["total"]:.2f}'
    for order in orders
    if order["paid"]
]

print(receipts)
```

```text
['A100: $82.50', 'A102: $120.00']
```

<!-- /quick -->

这段推导式按原订单顺序产生结果。它不会修改 `orders`，但结果中的元素不一定总与输入对象隔离；这里创建了新字符串，所以没有共享可变对象的问题。

把它展开成循环时，`for order in orders` 位于外层，`if order["paid"]` 位于内层，格式化和追加最后执行。遇到更复杂的推导式时，按这个方法反向展开，通常能迅速看清执行顺序。

### 条件表达式与过滤子句

这个例子先过滤 `None` 和负数，再对不低于 `20` 的价格打九折。开头的 `if ... else ...` 会为每个保留的价格选择一个结果，末尾的 `if` 则决定价格是否保留。

```python
# file: normalize_prices.py
prices = [12, 0, None, 27, -3, 40]

normalized = [
    round(price * 0.9, 2) if price >= 20 else float(price)
    for price in prices
    if price is not None and price >= 0
]

print(normalized)
```

```text
[12.0, 0.0, 24.3, 36.0]
```

`None` 和 `-3` 被过滤，因此不会参与数值比较或转换。`0` 不能写成简单的 `if price`，因为真值筛选会同时丢掉有效的零值；这里明确检查缺失值和允许范围。

条件表达式总要有 `else`。若需求只是保留高价项，应写 `[price for price in prices if price is not None and price >= 20]`；若每个有效价格都要保留，只是转换不同，条件表达式才是合适位置。

### 多个 `for` 子句

多个子句适合展平一层嵌套数据。这里先遍历部门，再遍历当前部门的成员，最后筛选启用成员；第二个 `for` 可以使用第一个 `for` 绑定的 `department`。

```python
# file: active_members.py
departments = [
    {
        "name": "sales",
        "members": [
            {"email": "ADA@EXAMPLE.COM", "active": True},
            {"email": "LIN@EXAMPLE.COM", "active": False},
        ],
    },
    {
        "name": "support",
        "members": [
            {"email": "KAI@EXAMPLE.COM", "active": True},
        ],
    },
]

addresses = [
    f'{department["name"]}:{member["email"].lower()}'
    for department in departments
    for member in department["members"]
    if member["active"]
]

print(addresses)
```

```text
['sales:ada@example.com', 'support:kai@example.com']
```

输出顺序先由部门顺序决定，再由各部门内部的成员顺序决定。推导式没有自动排序，也不会把同值结果去重；如果这些是需求，应明确选择 `sorted()` 或集合，并考虑它们是否改变业务语义。

两层 `for` 已经接近一眼能读懂的上限。若还需要多个筛选条件、异常处理或中间日志，应展开为具名循环；缩短行数本身不是使用推导式的理由。

### 只计算一次

筛选需要转换后的值，结果也要使用该值时，重复调用解析函数容易浪费工作。赋值表达式（assignment expression） `:=` 可以在筛选位置保存本轮结果，但它的作用域规则需要明确理解。

```python
# file: parse_scores_once.py
def parse_score(raw):
    try:
        return int(raw)
    except ValueError:
        return None


raw_scores = ["18", "skip", " 27 ", "-3", "42"]

scores = [
    score
    for raw in raw_scores
    if (score := parse_score(raw)) is not None and score >= 0
]

print(scores)
print(f"last parsed: {score}")
```

```text
[18, 27, 42]
last parsed: 42
```

每个原始字符串只调用一次 `parse_score()`。条件先排除 `None`，短路求值确保只有解析成功后才比较 `score >= 0`；负数解析成功，但被范围条件过滤。

这里的 `score` 在推导式结束后仍然存在，因为推导式内的赋值表达式把目标绑定到外层作用域。这是有意规定的特殊规则，不适合当作临时变量不会泄漏的技巧。如果这个绑定让读者意外，普通循环或小型辅助函数会更清楚。

## 陷阱

### 把副作用藏进结果表达式

> **陷阱:** `[send(message) for message in messages]` 会创建一个列表，即使调用方只关心 `send()` 的副作用。若函数返回 `None`，还会留下一个无用的 `None` 列表。
>
> **修复方法：** 使用普通 `for` 循环，让副作用和失败位置清楚可见。只有结果列表本身是需求时才使用列表推导式，并为异常传播编写测试。

推导式不会把副作用变成批处理事务。中途调用失败时，前面的调用可能已经生效，而列表赋值尚未完成。需要回滚、重试或逐项记录错误时，显式循环更容易表达控制权。

### 把过滤写成条件表达式

> **陷阱:** `[value if valid(value) for value in values]` 不是合法语法，因为结果位置的条件表达式缺少 `else`。模型根据其他语言的语法补全代码时，常会生成这种次序错误。
>
> **修复方法：** 丢弃无效项时写 `[value for value in values if valid(value)]`。保留每一项但选择不同结果时，写 `[value if valid(value) else fallback for value in values]`。

先用一句话说明需求中的动词会有帮助：「过滤」对应末尾子句，「替换」或「标记」对应开头条件表达式。两者并用时，先确认被过滤的输入不会执行结果表达式。

### 在筛选和转换中重复工作

> **陷阱:** `[parse(raw) for raw in rows if parse(raw) is not None]` 会对保留项调用两次 `parse()`。除了额外工作，带缓存、计数或时钟读取的函数还可能在两次调用间返回不同结果。
>
> **修复方法：** 对简单且易读的情况使用带括号的 `:=` 保存结果，或在普通循环中把解析、验证和追加分成三步。不要仅为少写一行而引入难懂的绑定。

还要检查异常语义。解析函数抛出异常时，筛选子句并不会自动跳过该项；它会终止整个推导式。要逐项容错，应让辅助函数返回明确的结果类型，或在循环中使用窄范围的 `try` 块。

### 让嵌套顺序失去领域含义

> **陷阱:** 多个 `for` 的次序决定遍历顺序和名称可见性。生成代码可能把内部、外部子句调换，得到不同排序、不同组合，或在绑定前引用名称。
>
> **修复方法：** 把推导式展开成嵌套循环，逐层写出输入基数和依赖名称。两层以上仍需解释时，保留展开后的循环，并给中间值起领域名称。

不要把嵌套推导式与多个 `for` 混为一谈。`[[transform(x) for x in row] for row in matrix]` 保留行结构，而 `[transform(x) for row in matrix for x in row]` 会展平一层。括号形状不同，结果形状也不同。

### 为一次消费创建完整列表

> **陷阱:** `sum([price * quantity for price, quantity in lines])` 会先构造完整列表，然后才开始求和。对很大的输入，这个中间列表可能没有任何后续用途。
>
> **修复方法：** 一次消费时写 `sum(price * quantity for price, quantity in lines)`，需要索引、重复遍历或修改结果时保留列表推导式。不要凭感觉声称某一种形式更快，应针对真实数据和运行时测量。

生成器表达式也不是自动更好。它通常只能消费一次，错误可能推迟到迭代时才出现，而且保留其引用可能延长输入资源的生命周期。选择边界取决于结果如何被使用，而不是只看输入大小。

### 在推导式里创建延迟调用的函数

> **陷阱:** `[lambda: item for item in items]` 创建的是一组以后才读取 `item` 的函数。调用发生在推导式结束后时，这些函数通常都会看到同一个最终绑定，而不是各轮值的快照。
>
> **修复方法：** 确实需要每轮值时，可用 `lambda item=item: item` 在创建函数时绑定默认参数，或调用一个接收 `item` 的命名工厂。若回调还捕获其他状态，优先使用工厂，让每次调用的所有权更明显。

立刻调用的结果表达式不会遇到这个时间差，问题出在结果本身是稍后执行的函数。测试应在推导式完成后再调用全部回调，并使用至少两个不同输入；只测试一个元素无法暴露共享绑定。

这个陷阱来自闭包的延迟名称查找，不是列表复制问题。修复时不要盲目复制整个输入对象；先确定回调应该捕获对象引用，还是创建时的某个不可变字段值。

<!-- deep -->

## 作用域与求值边界

推导式的迭代目标有独立的隐式作用域。若外层已有 `item`，执行 `[item * 2 for item in values]` 后，外层 `item` 的绑定不会被循环变量覆盖。这与普通 `for item in values` 不同；普通循环结束后，`item` 仍绑定到最后一个迭代值。

最左侧 `for` 的可迭代表达式是例外，它直接在外层作用域求值，然后交给推导式处理。后续的 `for`、所有过滤条件和结果表达式位于推导式的作用域语义中，因此可以依赖前面已经绑定的迭代目标。这个规则解释了为什么后续子句可以使用前面的名称，而外层却看不到那些迭代目标。

赋值表达式采用另一条特殊规则。在列表、集合、字典推导式或生成器表达式中，`:=` 的目标绑定到包含该推导式的作用域，而不是推导式的隐式作用域；已有 `global` 或 `nonlocal` 声明也会被遵守。因此，前例的 `score` 在推导式结束后仍能读取。

赋值目标不能与同一推导式中的任何迭代变量同名。`[item := item + 1 for item in values]` 会产生 `SyntaxError`，因为同一个名称不能既是推导式局部的迭代目标，又被指定为外层绑定。赋值表达式也不能出现在推导式任一 `in` 后的可迭代表达式中。

这些都是语言语义，不应依赖具体字节码形状。Python 可以改变实现方式，同时保留名称不泄漏和子句顺序等可观察行为。调试时优先展开源码和检查测试结果，不要根据某个 CPython 版本的反汇编推断可移植规则。

### 异常与部分求值

列表赋值只有在整个推导式成功后才发生，但结果表达式与筛选函数会逐项运行。中间抛出异常时，右侧正在构造的临时列表不会绑定到左侧名称；已经发生的外部副作用却不会自动撤销。这也是副作用不应藏在推导式里的更深层原因。

短路规则仍然适用。`if parsed is not None and parsed >= 0` 在第一项为假时不会计算第二项，因此能避免拿 `None` 与整数比较。多个连续过滤子句等价于用 `and` 逐层过滤，但拆成多个 `if` 并不会缓存重复的子表达式。

### 可变元素仍会共享

新列表不等于深拷贝。`[record for record in records]` 创建新的外层列表，但其中仍是原来的 `record` 对象；之后修改某个字典，会从两个列表中观察到变化。结果表达式创建新字符串、数字或新容器时，才会得到相应的新元素对象。

审查生成代码时，应分别问「外层容器是不是新的」和「内部对象是否共享」。需要复制记录时，显式写出复制策略，例如对浅层字典使用 `record.copy()`；嵌套所有权更复杂时，应先定义清楚需要隔离哪些节点，再选择合适的复制方式。

### 用不变量测试推导式

推导式很短，但测试不能只覆盖一个普通输入。它把遍历、筛选和转换压在一起，边界错误常表现为少一项、顺序变化、重复调用或错误的嵌套形状。最有效的测试直接对应这些可观察行为。

- 空输入应产生空列表，并且结果表达式不被调用。
- 同时包含保留项与丢弃项的输入，应验证调用次数和输出顺序。
- `0`、空字符串与 `None` 含义不同的领域，应分别覆盖，避免错误的真值筛选。
- 嵌套输入应至少包含两个外层元素和不同长度的内层元素，以暴露子句顺序和展平错误。

如果转换函数可能失败，再增加一个位于输入中间的失败样本。确认异常类型与传播位置，同时检查失败前是否已经产生外部副作用。对使用 `:=` 的代码，还应断言转换函数每个输入只调用一次。

属性测试也很适合纯推导式。例如，单纯筛选的结果长度不应超过输入长度，结果顺序应当是输入顺序的子序列。只有当这些性质确实属于需求时才断言，集合转换或显式排序会改变相应不变量。

### 解包目标与失败位置

`for` 后的目标可以使用序列解包。`[price * quantity for price, quantity in lines]` 要求每个输入项都能解包为恰好两个值；任何一项长度不符，都会在到达筛选和结果表达式之前抛出 `ValueError`。筛选子句不能用来挽救已经失败的解包。

遍历字典时，默认产出的是键。生成代码若写 `[key for key, value in mapping]`，通常会尝试把每个键本身解包成两个值，而不是取得键值对。需要键和值时，应明确调用 `mapping.items()`。

`zip()` 可以为多个输入提供解包后的元组，但默认在最短输入耗尽时停止。若不同长度本来是数据错误，静默截断会隐藏缺失项；Python 3.14 中可以使用 `zip(left, right, strict=True)`，让长度不一致抛出 `ValueError`。是否严格必须由数据契约决定。

解包失败、筛选失败和结果表达式失败发生在不同阶段。审查时为每个阶段准备一个失败样本，能确定异常是否出现在预期边界，也能发现生成代码把验证放得太晚。

### 输入迭代器的所有权

列表推导式会把输入迭代器从当前位置消费到耗尽。若调用方之后还要读取同一个迭代器，看到的将是剩余部分或空结果；推导式不会自动复制迭代器。对一次性数据流，谁负责消费必须在接口上说清楚。

中途抛出异常时，输入迭代器通常已经前进。重试整个推导式可能从中间继续，而不是重放原始数据。需要可重复重试时，应使用可重新创建的输入来源，或在明确接受内存成本后先物化一个稳定快照。

遍历期间修改源容器也会让行为难以判断。字典大小变化通常触发 `RuntimeError`，列表修改则可能跳过或重复处理元素。需要筛选掉元素时，构造新列表并在推导式完成后替换旧绑定；不要在结果表达式里改动正在遍历的容器。

这条所有权规则也适用于文件行、数据库游标和生成器。把它们交给推导式，意味着当前调用会立即推进输入；若资源必须在失败后关闭，应让上下文管理器包住整个消费过程。

## 相似语法，不同容器

列表推导式 `[expression for ...]` 会立即产生保留顺序且允许重复值的列表。集合推导式 `{expression for ...}` 会产生集合并去重，不应依赖其迭代或显示顺序。字典推导式 `{key: value for ...}` 需要键值表达式；遇到重复键时，后写入的值替代先前值。

生成器表达式 `(expression for ...)` 按需产出值，不会立即构造完整列表。它适合单次流式消费，但消费后不能像列表那样重新遍历同一批结果。是否替换列表推导式，必须从调用方的访问方式、错误时机和资源生命周期判断。

这些形式共享从左到右的 `for` 与 `if` 子句模型，却不共享容器语义。代码审查中仅把括号从 `[]` 改成 `{}` 或 `()`，可能改变重复值、顺序、求值时机和可重复消费性；这种改动应当视为行为变更。

<!-- /deep -->

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

## 延伸阅读

- [Python 教程：列表推导式](https://docs.python.org/3.14/tutorial/datastructures.html#list-comprehensions)
- [Python 语言参考：列表、集合与字典显示](https://docs.python.org/3.14/reference/expressions.html#displays-for-lists-sets-and-dictionaries)
- [PEP 202：列表推导式](https://peps.python.org/pep-0202/)
- [PEP 572：赋值表达式目标的作用域](https://peps.python.org/pep-0572/#scope-of-the-target)
