Python 生成器与迭代器
这篇解释
for循环背后发生了什么,以及yield为什么能处理放不进内存的数据。它们是 asyncio 协程的地基,也是 Python 惰性处理的核心机制。示例在 Python 3.13 全量验证。
迭代协议:for 循环的底层
任何能被 for 遍历的对象都是可迭代对象(实现 __iter__);for 循环的实际执行过程是:
it = iter(iterable) # 1. 调用 __iter__ 取得迭代器
while True:
try:
item = next(it) # 2. 反复调用 __next__
except StopIteration: # 3. 迭代器耗尽时抛 StopIteration,for 捕获后结束
break
...
迭代器是"一次性"的——耗尽后再 next() 只会得到 StopIteration:
nums = [1, 2, 3]
it = iter(nums) # list 是可迭代对象,iter() 返回它的迭代器
next(it) # 1
next(it) # 2
list(it) # [3](只剩最后一个)
next(it) # StopIteration
自己写一个迭代器要实现 __iter__ 和 __next__ 两个方法——而生成器用一次 yield 就全部代替。
生成器:yield 挂起函数
函数体内出现 yield,这个函数就变成了生成器函数。调用它不会执行函数体,而是返回生成器对象;每次 next() 执行到下一个 yield 处暂停,把值交出去,状态原地保留:
def countdown(n: int):
while n > 0:
yield n # 暂停点:交出 n,等下次 next()
n -= 1
gen = countdown(3)
next(gen) # 3(执行到第一个 yield)
next(gen) # 2(从暂停处继续)
list(gen) # [1](剩余部分一次取完)
for i in countdown(3): # 生成器天然可迭代
print(i)
生成器表达式是列表推导式的惰性版本——把方括号换成圆括号:
squares = [x * x for x in range(5)] # 列表:立即全部计算,占内存
lazy = (x * x for x in range(5)) # 生成器:一个元素都不曾计算
next(lazy) # 0,此时才计算第一个
sum(x * x for x in range(10)) # 传给函数时括号可省略一层
为什么惰性有价值
# 逐行处理 10GB 的日志,内存占用恒定:
def error_lines(path: str):
with open(path, encoding="utf-8") as f:
for line in f: # 文件对象本身就是惰性迭代器
if "ERROR" in line:
yield line.strip()
n = sum(1 for _ in error_lines("big.log")) # 全程不把文件读进内存
# 无限序列:只有惰性才能表达"无穷"
def naturals():
i = 0
while True:
yield i
i += 1
g = naturals() # 只创建一次,反复推进同一个生成器
first = [next(g) for _ in range(5)] # [0, 1, 2, 3, 4]
yield from:委托给另一个可迭代对象
def flatten(nested):
for item in nested:
if isinstance(item, list):
yield from flatten(item) # 委托给子生成器,逐项转交
else:
yield item
list(flatten([1, [2, [3, 4]], 5])) # [1, 2, 3, 4, 5]
yield from iterable 等价于"把它逐项 yield 出来",但还建立了双向通道(子生成器的返回值、异常都会传递),因此也是协程的前身。
进阶:send 与双向通道
yield 不只可以交出值,也能接收值——调用方用 send() 恢复生成器,同时传入数据:
def running_average():
total, count = 0.0, 0
avg = None
while True:
value = yield avg # yield 的值交给调用方;send 的值赋给 value
total += value
count += 1
avg = total / count
avg = running_average()
next(avg) # 预激活:推进到第一个 yield(不能用 send(None) 之外的东西启动)
avg.send(10) # 10.0
avg.send(20) # 15.0
生成器协程(asyncio 的前身)就建立在这条双向通道上;日常数据处理用不到 send,但读旧代码会遇到。
itertools 精选
from itertools import count, cycle, chain, islice, groupby, product
count(10, 2) # 无限计数:10 12 14 ...(配 islice 取前几个)
list(islice(count(10, 2), 4)) # [10, 12, 14, 16]
list(chain([1, 2], "ab")) # [1, 2, 'a', 'b']:串联多个可迭代对象
list(islice(cycle("AB"), 5)) # ['A','B','A','B','A']:无限循环
for key, group in groupby(["aa", "ab", "bb", "cc"], key=lambda s: s[0]):
print(key, list(group)) # a ['aa','ab'] b ['bb'] c ['cc'](相邻分组,需先排序)
list(product("AB", "12")) # [('A','1'),('A','2'),('B','1'),('B','2')]:笛卡尔积
常见错误
| 错误 | 说明 |
|---|---|
| 生成器只能遍历一次 | 耗尽后再遍历得到空结果;要多次使用先 list() 物化 |
len(gen) 不存在 |
生成器没有长度;需要长度就说明该用列表 |
| 创建时不执行任何代码 | 忘记迭代时函数体一行都不会跑;返回值也拿不到(要用 yield from 或 StopIteration.value) |
groupby 忘记先排序 |
它只对相邻的相同键分组 |
| 在生成器里持有资源忘了关闭 | 用 with 包裹(生成器被 close()/GC 时会正常执行清理),或显式 .close() |
| 迭代中修改被遍历的容器 | dict/list 迭代期间增删键会抛 RuntimeError 或行为异常 |
相关:asyncio 把"暂停点"从 yield 升级为 await;Python 并发讨论什么时候需要它们。