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 fromgenerator_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 的协程发展经历了几个阶段:

  1. 生成器协程(Python 2.5+):用 yieldsend() 模拟协程行为
  2. 基于生成器的协程(Python 3.4+):用 @asyncio.coroutineyield from
  3. 原生协程(Python 3.5+):用 async defawait

你想想看,从 yieldyield 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.coroutineasync def。虽然它们能共存,但调试起来非常痛苦。建议新项目一律使用原生协程。老代码如果还能跑,也别急着改,但新写的代码一定要用 async/await

4.6 小结

这一章我们聊了:

  • 生成器如何用 yield 实现暂停和恢复
  • send() 方法让生成器具备了双向通信能力
  • yield from 简化了生成器的嵌套调用
  • 协程的本质就是「可暂停的函数」,跟生成器一脉相承

下一章,我们会正式进入 async/await 的世界。到时候你会发现,有了生成器的基础,理解协程简直水到渠成。

嗯,今天就到这里。代码多敲几遍,尤其是 send()yield from 的例子。动手试试,比看十遍都管用。