# Python 生成器：yield、迭代器，以及列表和字典的推导式

URL: https://skillaria.top/zh/guides/python/generatory-python

生成器是按需逐个产生值的对象，不会把全部数据保存在内存中；它通过包含 yield 关键字的函数或圆括号中的生成器表达式创建。列表、字典和集合推导式是用一行代码构建完整集合的简写，例如 [x * 2 for x in xs]、{k: v for k, v in pairs}、{x for x in xs}。

Python 中的生成器是一个对象，它按需逐个产生值，不会把整个序列保存在内存中。创建生成器有两种方式：包含关键字 `yield` 的函数，或圆括号中的生成器表达式。与之相关的还有列表、字典和集合的推导式（comprehension），它是用一行代码构建完整集合的简写形式。下面讲解 python yield 的工作原理，生成器与迭代器的区别，以及如何避免把推导式写成难以阅读的一长串代码。

## 迭代器：for 循环的基础

要理解生成器，先要了解迭代器。可迭代对象（iterable）是可以用循环遍历的对象：列表、字符串、字典、文件。迭代器是一个会记住当前位置的对象，每次调用 `next()` 就返回下一个元素；元素耗尽后，它会抛出 `StopIteration`。

```
nums = [10, 20, 30]
it = iter(nums)      # 从列表获取迭代器
print(next(it))      # 10
print(next(it))      # 20
print(next(it))      # 30
next(it)             # StopIteration
```

`for` 循环做的正是这件事：先调用 `iter()`，然后反复调用 `next()`，直到出现 `StopIteration`。你也可以用一个类实现自己的迭代器，只需定义 `__iter__` 和 `__next__` 方法：

```
class Countdown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current  0:
        yield start
        start -= 1

gen = countdown(3)
print(gen)          #
print(next(gen))    # 3
print(list(gen))    # [2, 1] — 从上次停下的地方继续
```

当函数运行到末尾（或遇到 `return`）时，生成器会自动抛出 `StopIteration`，`for` 循环随之正常结束。这就是 `yield` 与 `return` 的主要区别：`return` 结束函数并返回一个值，`yield` 则暂停函数，并且可以产出任意多个值。

### 生成器与迭代器的区别

任何生成器都是迭代器：它有 `__iter__` 和 `__next__`。但并非任何迭代器都是生成器。区别在于创建方式和功能。

|  | 迭代器（类） | 生成器 |
| --- | --- | --- |

| 如何创建 | 带有 `__iter__` 和 `__next__` 的类 | 包含 `yield` 的函数，或 `(x for x in ...)` |

| 状态 | 手动保存在属性中 | 自动保存在局部变量中 |

| StopIteration | 需要自己抛出 | 函数结束时自动产生 |

| 其他功能 | 取决于你的实现 | `send()`、`throw()`、`close()` |

它们有一个共同的重要特性：只能遍历一次。对已耗尽的生成器再次执行 `for` 不会产出任何值——需要重新创建。

### 无限生成器与读取大文件

生成器不一定要有结尾。无限序列是安全的，只要你只取其中有限的部分：

```
from itertools import islice

def ids():
    n = 1
    while True:
        yield f"ID-{n:04d}"
        n += 1

print(list(islice(ids(), 3)))  # ['ID-0001', 'ID-0002', 'ID-0003']
```

最实用的场景是逐行处理大文件。生成器可以串联成流水线，任何时候内存中只有一行，而不是整个文件：

```
def read_lines(path):
    with open(path, encoding="utf-8") as f:
        for line in f:
            yield line.rstrip("\n")

def only_errors(lines):
    for line in lines:
        if "ERROR" in line:
            yield line

for line in only_errors(read_lines("app.log")):
    print(line)
```

### yield from 与 send

`yield from` 会把另一个可迭代对象的所有值依次产出，省去嵌套循环：

```
def flatten(groups):
    for g in groups:
        yield from g

print(list(flatten([[1, 2], [3], [4, 5]])))  # [1, 2, 3, 4, 5]
```

方法 `send()` 可以把值传入生成器：它会成为 `yield` 表达式的结果。在第一次调用 `send` 之前，必须先用 `next()` 启动生成器。下面是一个计算平均值的例子：

```
def averager():
    total, count = 0, 0
    avg = None
    while True:
        value = yield avg
        total += value
        count += 1
        avg = total / count

a = averager()
next(a)            # 执行到第一个 yield
print(a.send(10))  # 10.0
print(a.send(20))  # 15.0
```

在实际代码中 `send` 很少用到，但面试时经常会被问到。

### range 是生成器吗？

不是。`range` 是惰性序列：它同样不保存全部数字，但支持索引、`len()`、`in` 检查，并且可以遍历任意多次。生成器则不具备这些功能。

```
r = range(5)
print(r[2], len(r), 3 in r)  # 2 5 True
print(list(r), list(r))      # 两次都是 [0, 1, 2, 3, 4]
```

## 列表推导式：list comprehension

列表推导式的形式是 `[表达式 for x in 集合]`。它创建一个新列表，不修改原列表。阅读顺序是从左到右：先写“放入什么”，再写“从哪里取”。

```
amounts = [100, 250, 400]

# 普通循环
with_markup = []
for a in amounts:
    with_markup.append(a * 1.2)

# 同样的操作，用推导式
with_markup = [a * 1.2 for a in amounts]
print(with_markup)  # [120.0, 300.0, 480.0]
```

虽然名字里有“列表生成器”，但结果是一个普通的已完成列表，而不是生成器：所有元素都是立即计算的。

### 带 if 和 if else 条件的列表推导式

这里最容易混淆。过滤条件 `if` 放在末尾，用于丢弃元素。选择条件（三元运算符）放在开头，把每个元素变成两个值中的一个，而不丢弃任何元素。

```
amounts = [100, 250, 400]

big = [a for a in amounts if a > 200]                   # [250, 400]
labels = ["多" if a > 200 else "少" for a in amounts]
# ['少', '多', '多']

# 过滤与选择一起使用
parity = ["偶" if n % 2 == 0 else "奇" for n in range(6) if n > 2]
# ['奇', '偶', '奇']
```

写成 `[a for a in amounts if a > 200 else 0]` 是语法错误：`else` 只能用于开头的条件表达式。

### 嵌套推导式与二维列表

两个 `for` 的书写顺序与嵌套循环相同，从外到内。这有助于在展开列表的列表时不搞错顺序：

```
groups = [[1, 2], [3], [4, 5]]
flat = [x for g in groups for x in g]
# 等价于：
# for g in groups:
#     for x in g:
#         flat.append(x)
print(flat)  # [1, 2, 3, 4, 5]
```

二维列表（矩阵）可以用推导式套推导式来创建。经典的错误是对列表做乘法：各行会是同一个对象。

```
bad = [[0] * 3] * 2
bad[0][0] = 1
print(bad)   # [[1, 0, 0], [1, 0, 0]] — 两行都变了

grid = [[0] * 3 for _ in range(2)]
grid[0][0] = 1
print(grid)  # [[1, 0, 0], [0, 0, 0]]

# 转置
m = [[1, 2, 3], [4, 5, 6]]
print([[row[i] for row in m] for i in range(3)])  # [[1, 4], [2, 5], [3, 6]]
```

## 字典与集合推导式

同样的方法适用于其他集合。带冒号的花括号构建字典（dict comprehension），不带冒号的花括号构建集合（set comprehension）。

```
spent = [("餐饮", 500), ("打车", 300), ("餐饮", 200)]

categories = {cat for cat, _ in spent}
print(categories)  # {'餐饮', '打车'} — 集合的顺序不保证

last = {cat: amount for cat, amount in spent}
print(last)        # {'餐饮': 200, '打车': 300}

squares = {n: n ** 2 for n in range(4)}  # {0: 0, 1: 1, 2: 4, 3: 9}
swapped = {v: k for k, v in squares.items()}
```

注意 `last`：键重复时以最后一个值为准，500 这笔金额丢失了。如果需要求和，推导式不合适——应该写循环，或使用 `collections.Counter` / `defaultdict`。

### 元组推导式？

Python 中没有单独的“元组推导式”。圆括号产生的是生成器表达式，要得到元组，需要把它传给 `tuple()`：

```
t = tuple(n * 2 for n in range(3))  # (0, 2, 4)
```

## 生成器表达式：不保存，只产出

把方括号换成圆括号，就得到了生成器表达式。它不会预先计算任何东西，而是逐个产出值。对于一百万个元素，内存差别非常大：

```
import sys

as_list = [n for n in range(1_000_000)]
as_gen = (n for n in range(1_000_000))
print(sys.getsizeof(as_list))  # 约 8 MB
print(sys.getsizeof(as_gen))   # 几百字节
```

生成器表达式最常用于聚合函数的参数中。如果生成器是函数的唯一参数，可以省略多余的括号。`any()` 和 `all()` 在得到第一个结果后就会停止，不会遍历剩余部分。

```
spent = [("餐饮", 500), ("打车", 300), ("餐饮", 200)]

total = sum(a for _, a in spent)            # 1000
has_big = any(a > 400 for _, a in spent)    # True，找到 500 后就不再继续
biggest = max(spent, key=lambda p: p[1])    # ('餐饮', 500)
```

选择规则很简单：如果结果需要多次使用、需要索引或 `len()`，就用列表；如果只需要遍历一次并计算某个结果，就用生成器。

## 作用域：循环变量不会泄漏

这是即使是有经验的开发者也会踩的坑：

```
x = 10
squares = [x for x in range(3)]
print(x)  # 10
```

推导式中的循环变量位于自己的作用域内，不会覆盖外部的同名变量（在 Python 3 中如此；在 Python 2 中并非如此）。普通的 `for` 循环结束后，`x` 会变成 2。从 3.12 版本开始（PEP 709），推导式被内联到外围函数中，运行更快，但变量可见性的规则保持不变。

## 3.15 中推导式内的 * 与 ** 解包（PEP 798）

在函数调用和列表字面量中，星号解包早就可以使用，但在推导式内部曾是 `SyntaxError`。在 Python 3.15 中这个限制被取消：`*元素` 表示“把它的所有元素都放在这里”。它适用于列表、集合和生成器，包括异步的情形。

```
# Python 3.15+
groups = [[1, 2], [3], [4, 5]]
flat = [*g for g in groups]        # [1, 2, 3, 4, 5]
uniq = {*g for g in groups}        # 集合
lazy = (*g for g in groups)        # 惰性生成器
```

双星号对字典做同样的事情。这对于分层配置非常方便：“默认值 → 文件 → 命令行标志”，每一层都会覆盖前一层的键。

```
# Python 3.15+
layers = [{"debug": False, "port": 80}, {"port": 8080}, {"debug": True}]
config = {**layer for layer in layers}
# {'debug': True, 'port': 8080}
```

课程中提到两点注意事项。第一，不能把星号与不加括号的条件表达式混用：`[*w if w else 0 for w in weeks]` 是语法错误，条件需要加括号。第二，这种代码无法在 3.14 及更早版本中运行：如果库需要支持旧版本，请保留双重 `for`，或使用 `itertools.chain.from_iterable`。

## 异步生成器

如果 `async def` 函数中包含 `yield`，它就是异步生成器。遍历它要使用 `async for`，函数内部可以用 `await` 等待网络或磁盘。异步推导式也是存在的。

```
import asyncio

async def ticks(n):
    for i in range(n):
        await asyncio.sleep(0.1)
        yield i

async def main():
    result = [i async for i in ticks(3)]
    print(result)  # [0, 1, 2]

asyncio.run(main())
```

## 常见错误

- **三层嵌套的推导式。** 两个 `for`、两个 `if` 和三元运算符挤在一行里，一周后就没人能看懂了。如果放不进一行，就写普通循环：PEP 8 把可读性放在简洁性之上。
- **重复遍历生成器。** 第二次执行 `sum(gen)` 会得到 0，因为生成器已经耗尽。
- **为了副作用使用推导式。** `[print(x) for x in xs]` 会构建一个无用的 `None` 列表。执行操作应该用 `for` 循环。
- **期望生成器表达式可以取下标。** `(x for x in xs)[0]` 会得到 `TypeError`：生成器没有索引。

## 生成器练习题

1. 编写生成器 `fib()`，无限产出斐波那契数，并用 `islice` 取前 10 个。
2. 用一行代码得到长度大于三个字母的单词的字典：“单词 → 长度”。
3. 不创建列表，计算一百万以内所有偶数平方的和。
4. 编写生成器 `chunks(seq, n)`，每次产出列表中的 `n` 个元素。

## 小结

迭代器通过 `next()` 逐个返回元素，而生成器是编写迭代器最简单的方式：带有 yield 的函数或圆括号中的表达式。列表、字典和集合推导式用一行代码构建完整集合：过滤条件 `if` 放在末尾，选择条件 `if else` 放在开头。需要节省内存或只处理一次数据流时，使用生成器；需要多次使用结果时，使用列表。推导式、生成器以及 Python 3.15 的新解包语法，都会在课程中结合支出记录器项目进行实践讲解。

## FAQ

**生成器函数中可以使用 return 吗？**

可以。不带值的 return 只是结束生成器；带值的 return 会把值放入 StopIteration 异常的 value 属性中，使用 yield from 的表达式会得到这个值。

**如何把生成器转换为列表？**

把它传给 list()：list(gen)。注意，转换之后生成器就耗尽了，而且所有值都会占用内存。

**列表推导式和带 append 的循环哪个更快？**

通常推导式稍快，因为它不需要在每次迭代中调用 append 方法。但选择时应该优先考虑可读性，性能差异很少是决定性的。

**如何判断一个对象是否为生成器？**

对对象使用 inspect.isgenerator(obj)，对函数使用 inspect.isgeneratorfunction(f)；判断对象是否为迭代器，可以用 isinstance(obj, collections.abc.Iterator)。

**生成器表达式和列表推导式有什么区别？**

方括号会立即在内存中构建整个列表；圆括号则创建一个生成器，按需计算元素，并且只能遍历一次。


---
Source: Skillaria.Top (https://skillaria.top)
