处理大文件或数据流时,把所有数据加载进内存是不现实的。生成器提供了一种"按需生成"的方式,解决了这个问题。
迭代器协议
Python 的 for 循环本质上是在调用迭代器协议:
# for 循环等价于这段代码
it = iter(my_list) # 调用 __iter__,返回迭代器
while True:
try:
item = next(it) # 调用 __next__,取下一个元素
# 执行循环体
except StopIteration:
break
实现了 __iter__ 和 __next__ 的对象就是迭代器。
生成器函数
用 yield 的函数就是生成器函数,调用它返回一个生成器对象:
def count_up(start, end):
current = start
while current <= end:
yield current # 暂停,返回 current,等待下次调用
current += 1
for n in count_up(1, 5):
print(n) # 1 2 3 4 5
关键在于暂停和恢复:每次 next() 调用,函数从上次 yield 的地方继续执行,直到下一个 yield。
实际用处:处理大文件
# 不好:一次性加载所有行到内存
def read_all_lines(filename):
with open(filename) as f:
return f.readlines() # 10GB 文件直接 OOM
# 好:逐行生成,内存始终只有一行
def read_lines(filename):
with open(filename) as f:
for line in f:
yield line.strip()
# 使用
for line in read_lines("huge_file.csv"):
process(line)
生成器表达式
列表推导式的"惰性版本":
# 列表推导式:立刻计算,全部存内存
squares_list = [x**2 for x in range(1_000_000)] # 占用大量内存
# 生成器表达式:按需计算,几乎不占内存
squares_gen = (x**2 for x in range(1_000_000))
当你只需要遍历一次,不需要随机访问时,生成器表达式永远比列表推导式更省内存。
yield from:委托给子生成器
def flatten(nested):
for item in nested:
if isinstance(item, list):
yield from flatten(item) # 递归委托
else:
yield item
data = [1, [2, 3, [4, 5]], 6]
print(list(flatten(data))) # [1, 2, 3, 4, 5, 6]
yield from 比手动 for item in sub_gen: yield item 更简洁,也更高效。
send():双向通信
生成器不只能输出数据,还能接收数据:
def accumulator():
total = 0
while True:
value = yield total # yield 既输出 total,又等待接收下一个值
if value is None:
break
total += value
acc = accumulator()
next(acc) # 启动生成器,到达第一个 yield
acc.send(10) # 发送 10,total = 10
acc.send(20) # 发送 20,total = 30
acc.send(5) # 发送 5,total = 35
这个特性是协程(asyncio)的基础——async/await 在底层正是用生成器的 send 机制实现的。
什么时候用生成器
- 数据集太大,无法全部加载进内存
- 数据是流式产生的(网络流、实时日志)
- 只需要遍历一次,不需要随机访问
- 需要表达无限序列(斐波那契数列、自增 ID)
生成器是 Python 惰性求值的核心工具,理解它也是理解 asyncio 的前置知识。