理解了生成器函数的惰性求值机制后,就会很自然地遇到一种更简洁的“轻量级生成器”——生成器表达式。
什么是生成器表达式
它的语法和列表推导式几乎一样,只是把方括号 [] 换成圆括号 ():
# 列表推导式:一次性生成整个列表,占内存
squares_list = [x**2 for x in range(10)]
# 生成器表达式:返回一个生成器对象,按需取值
squares_gen = (x**2 for x in range(10))
- 生成器表达式不会立即计算所有结果,而是返回一个生成器对象。
- 当你用
next()或在for循环中迭代它时,才一次产生一个值。
为什么要用它——内存友好
假设你有一个百万级别的数据流,想找出所有满足条件的元素并做大写转换。如果用列表推导式:
# 先产生一个百万元素的列表,占大量内存
filtered = [line.upper() for line in huge_file if condition]
而生成器表达式只是声明了一个“计算管道”,内存占用极小:
# 只是一个生成器对象,几乎不占额外内存
filtered = (line.upper() for line in huge_file if condition)
for item in filtered:
process(item)
最佳实践
- 当生成器表达式直接作为唯一参数传给函数时,可以省略外层的圆括号,比如:
sum(x**2 for x in range(1000)) # 比 sum([x**2 for ...]) 更高效
max(len(word) for word in words)
- 读文件时组合使用生成器表达式和生成器,可以处理大于内存的日志或数据集,例如:
lines = (line.strip() for line in open('big.log'))
errors = (line for line in lines if 'ERROR' in line)
for err in errors:
print(err)
这里管道中的每一步都是惰性的,只有逐条迭代时才触发真正的计算。
8.6.4 协程基础
生成器除了产出值,还可以接收值——这时它就变成了协程。理解协程是后续学习 asyncio 和异步编程的重要前置知识。
从 yield 到双向通信
普通生成器用 yield 向外发送数据;而协程可以通过 yield 接收外界发送的数据,形成双向交互:
def receiver():
print("开始接收")
while True:
msg = yield # 接收 send 传来的值,赋值给 msg
print(f"收到: {msg}")
g = receiver()
next(g) # 预激活,让生成器执行到 yield 处
g.send("hello") # 输出:收到: hello
g.send("world") # 输出:收到: world
g.close() # 关闭协程
yield在赋值语句的右边,意味着它既可能传递值出去,也可能接收从外面send()进来的值。- 使用协程前必须先用
next()或send(None)将其“启动”到第一个yield位置,否则会抛出TypeError。
协程能做什么
协程内部可以持有自己的状态,接收数据并做清理、累加等操作,非常适合实现状态机、数据流处理器、简单调度器。
def averager():
total = 0
count = 0
average = None
while True:
x = yield average
total += x
count += 1
average = total / count
avg = averager()
next(avg) # 启动
print(avg.send(10)) # 10.0
print(avg.send(20)) # 15.0
print(avg.send(30)) # 20.0
协程与 asyncio 的区别
- Python 早期(3.3 之前)的协程就是指上面这种基于生成器的 “yield 协程”。
- 从 Python 3.5 开始,官方引入了
async def/await的原生协程,搭配asyncio实现异步 IO。这是更现代的用法。 - 但理解生成器协程的
send()、throw()、close()等接口,能帮你深度掌握协程调度原理,也为后续学习事件循环打下基础。
一句话总结:生成器表达式让你用最简语法写出惰性数据管道;协程则让生成器从“产出数据”升级为“可收发数据的双向通道”,是实现复杂数据流控制和异步编程的原始基石。