怎样通过一个案例理解Python的协程在底层与生成器的关系

访客 源码剖析 1

本文目录导读:

  1. 核心关系一句话
  2. 案例:手动实现一个协程调度器(模拟操作系统)
  3. 关键底层机制:状态机 + 栈帧
  4. 从生成器到协程的完整演进图
  5. 现实中的对比
  6. 一句话总结

要理解 Python 协程与生成器的底层关系,最经典的案例是 “消费者-生产者模型”,通过它你能清晰看到生成器如何一步步演化成协程


核心关系一句话

协程 = 生成器 + send() + yield from
生成器是协程的骨架,send() 赋予了它双向通信能力,yield from 实现了协程的嵌套组合。


案例:手动实现一个协程调度器(模拟操作系统)

我们先从一个普通生成器开始,逐步改成协程。

生成器(单向数据流)

def simple_gen():
    """一个普通的生成器,只能产出值"""
    count = 0
    while True:
        yield count
        count += 1
gen = simple_gen()
print(next(gen))  # 0
print(next(gen))  # 1
  • 此时:生成器只能向外 yield,无法接收外部输入。
  • 数据流:生成器 → 调用方(单向)。

生成器变身为协程(双向数据流)

加入 send(),生成器就能从外部接收值:

def coro_producer():
    """一个可以接收数据的生成器(即协程)"""
    while True:
        # 暂停并等待外部通过send()传入值
        item = yield  
        print(f"[协程] 处理了: {item}")
c = coro_producer()
next(c)          # 预激活:让协程停在yield处,准备接收数据
c.send("苹果")   # 输出:[协程] 处理了: 苹果
c.send("香蕉")   # 输出:[协程] 处理了: 香蕉
  • 底层发生了什么?

    1. next(c) 让生成器执行直到遇到 yield,挂起。
    2. c.send("苹果") 将值注入yield 表达式的返回值,即 item = "苹果"
    3. 生成器继续执行,打印后回到循环顶部,yield 挂起,等待下一次 send()
  • 到此:生成器具备了协程的核心能力——可暂停、可恢复、可接收外部数据


yield from 实现协程嵌套(复杂流程)

yield from 是生成器到协程的关键升级,它让协程可以委托子生成器执行,形成类似函数调用的效果。

def sub_coro():
    """子协程"""
    total = 0
    while True:
        x = yield       # 接收外部数据
        total += x
        print(f"子协程当前总和: {total}")
def main_coro():
    """主协程,使用yield from委托子协程"""
    yield from sub_coro()
# 使用
c = main_coro()
next(c)          # 预激活
c.send(10)       # 子协程当前总和: 10
c.send(20)       # 子协程当前总和: 30
  • 底层原理

    • yield from 会在主协程和子协程之间建立透明管道
    • 所有 send() 调用会直接穿透到子协程内部的 yield
    • 当子协程终止时,会向主协程返回一个值(类似函数 return)。
  • 这个能力让协程能像函数一样组合,是 asyncio 等框架的基础。


关键底层机制:状态机 + 栈帧

Python 的生成器/协程底层是一个函数 + 一个栈帧对象

def demo():
    a = 1
    b = yield a
    yield b
gen = demo()
print(gen.gi_frame.f_locals)  # 查看局部变量
  • 每次 yield 时,Python 会保存当前栈帧的完整状态(局部变量、指令指针等)。
  • 调用 next()send() 时,恢复该栈帧继续执行。
  • 协程本质就是:可以多次暂停/恢复的栈帧。

从生成器到协程的完整演进图

特性 普通生成器 增强生成器 协程
数据方向 单向产出 可接收数据 双向+嵌套
核心操作 yield yield + send() yield from
暂停原因 产出值 等待输入 等待输入/子协程
典型用途 迭代器 简单管道 异步I/O、状态机

现实中的对比

# 生成器风格:迭代数据
def read_lines():
    for line in open("file.txt"):
        yield line.strip()
# 协程风格:事件循环调度
import asyncio
async def read_async():
    data = await fetch_url("http://...")  # await = yield from
    return data
  • async def 定义的函数,其底层代码在 Python 3.5+ 中仍会生成一个特殊的生成器对象<class 'coroutine'>)。
  • await 在底层被翻译为 yield from,只是语法糖和类型检查不同。

一句话总结

你可以把协程看作“升级版生成器”

  • 生成器是人→机器的单向送料器(next()拿结果)。
  • 协程是双向对讲机send()传数据,yield返回结果),并支持yield from实现子任务委派,这正是 asyncio 调度数百个网络请求的底层基础。

标签: 协程 生成器

抱歉,评论功能暂时关闭!