4、协程入门:生成器与yield、yield from语法、协程的初步概念
好,咱们今天聊点硬核的。
协程,这个词你肯定听过。但说实话,很多人学了半天,还是搞不清它到底是个啥。我当年刚接触 Python 异步编程时,也是一脸懵。后来我发现,想理解协程,得先从生成器入手。你想想看,生成器其实已经具备了协程的雏形。
4.1 生成器:协程的前身
生成器是什么?说白了,就是一个可以暂停的函数。
普通函数跑起来,一口气执行到底。生成器不一样,它跑着跑着可以停下来,把控制权交还给你,等你下次再叫它,它从停下的地方继续跑。
这个能力,就是协程的基础。
核心概念:生成器函数使用 yield 关键字,每次调用 next() 或 send() 时,执行到下一个 yield 处暂停。
看个最简单的例子:
def simple_generator():
print("第一次调用")
yield 1
print("第二次调用")
yield 2
print("第三次调用")
yield 3
gen = simple_generator()
print(next(gen)) # 输出:第一次调用 1
print(next(gen)) # 输出:第二次调用 2
print(next(gen)) # 输出:第三次调用 3
看到了吗?每次 next() 只执行到下一个 yield。函数的状态——局部变量、执行位置——都被保留着。这就是「暂停-恢复」机制。
我的经验:我在项目中用生成器处理过几百万行的日志文件。如果用列表一次性加载,内存直接爆掉。生成器逐行读取,内存占用几乎为零。这个特性,后来成了我理解协程的钥匙。
4.2 yield 的进阶用法:双向通信
很多人以为 yield 只能往外吐数据。其实它还能接收数据。
嗯,这里要注意。生成器对象有一个 send() 方法,可以把值传进生成器内部。被 yield 接收。
def echo_generator():
while True:
received = yield
print(f"收到:{received}")
gen = echo_generator()
next(gen) # 启动生成器,执行到 yield 处暂停
gen.send("你好") # 输出:收到:你好
gen.send("世界") # 输出:收到:世界
为什么会这样?因为 yield 在这里既是一个出口,也是一个入口。它把值吐出去的同时,也能把外面传进来的值接住。
这个特性,让生成器不再只是「数据生产者」,它变成了一个可以跟外部交互的「协程雏形」。
避坑指南:我曾经在第一次使用 send() 时忘了先调用 next() 启动生成器,结果直接报 TypeError: can't send non-None value to a just-started generator。记住:生成器刚创建时,停在函数开头,还没到 yield 处,必须先 next() 一次,让它跑到 yield 那里等着。
4.3 yield from:生成器的语法糖
如果你写过嵌套的生成器,一定遇到过这种痛苦:
def generator_a():
for item in generator_b():
yield item
这种「把子生成器的每个值挨个 yield 出来」的写法,太啰嗦了。
yield from 就是来解决这个问题的。它可以直接委托给另一个生成器。
def generator_b():
yield 1
yield 2
yield 3
def generator_a():
yield from generator_b()
yield 4
for value in generator_a():
print(value) # 输出:1 2 3 4
你看,yield from 把 generator_b() 的所有值「展开」了。代码简洁多了。
但 yield from 不止于此。它还能处理 send() 和异常传递。子生成器跟调用者之间的双向通信,yield from 全帮你打通了。
深入理解:yield from 本质上是一个「双向通道」。调用者通过 send() 发送的值,会直接传给子生成器;子生成器 return 的值,也会被 yield from 表达式捕获。
def sub_gen():
data = yield "请发送数据"
return f"收到:{data}"
def main_gen():
result = yield from sub_gen()
print(result)
gen = main_gen()
print(next(gen)) # 输出:请发送数据
gen.send("测试数据") # 输出:收到:测试数据
这个例子展示了 yield from 的完整能力。它让生成器的嵌套调用变得像普通函数调用一样自然。
4.4 协程的初步概念
好了,铺垫了这么多,咱们终于可以聊聊协程了。
协程,说白了就是「可以暂停和恢复执行的函数」。它跟生成器的区别是什么?
| 特性 | 生成器 | 协程 |
|---|---|---|
| 主要用途 | 生成数据序列 | 协作式多任务 |
| 暂停方式 | yield | await |
| 数据流向 | 单向(向外) | 双向 |
| 驱动方式 | next() / for 循环 | 事件循环 |
我个人习惯把协程理解为「升级版的生成器」。生成器用 yield 暂停,协程用 await 暂停。生成器靠手动 next() 驱动,协程靠事件循环自动调度。
但它们的核心机制是一样的:保存状态,暂停执行,等待时机,恢复运行。
我的理解:你可以把协程想象成一个「会自己暂停的线程」。但它比线程轻量得多。线程的切换由操作系统控制,协程的切换由程序自己控制。我在做高并发 I/O 服务时,用协程替代线程,性能提升了 3 倍以上。因为协程没有线程上下文切换的开销。
4.5 从生成器到协程的演进
Python 的协程发展经历了几个阶段:
- 生成器协程(Python 2.5+):用
yield和send()模拟协程行为 - 基于生成器的协程(Python 3.4+):用
@asyncio.coroutine和yield from - 原生协程(Python 3.5+):用
async def和await
你想想看,从 yield 到 yield from,再到 async/await,语法越来越简洁,语义越来越清晰。但底层的「暂停-恢复」机制,始终没变。
# 生成器协程(旧式)
def old_style_coroutine():
data = yield
print(f"处理:{data}")
# 基于生成器的协程(Python 3.4)
@asyncio.coroutine
def old_async_coroutine():
result = yield from some_io_operation()
# 原生协程(Python 3.5+)
async def modern_coroutine():
result = await some_io_operation()
避坑指南:我曾经在维护一个老项目时,看到代码里混用了 @asyncio.coroutine 和 async def。虽然它们能共存,但调试起来非常痛苦。建议新项目一律使用原生协程。老代码如果还能跑,也别急着改,但新写的代码一定要用 async/await。
4.6 小结
这一章我们聊了:
- 生成器如何用
yield实现暂停和恢复 send()方法让生成器具备了双向通信能力yield from简化了生成器的嵌套调用- 协程的本质就是「可暂停的函数」,跟生成器一脉相承
下一章,我们会正式进入 async/await 的世界。到时候你会发现,有了生成器的基础,理解协程简直水到渠成。
嗯,今天就到这里。代码多敲几遍,尤其是 send() 和 yield from 的例子。动手试试,比看十遍都管用。