KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

02 · 双向通道:send / throw / close 与 yield from — keel 龙骨

01 章的生成器是「单行道」:函数往外吐值,调用方只会要。这一章把另外三个方向补齐——往里塞值、往里扔异常、要求关闭——然后学 yield from 这个委托语法。这些不是炫技语法,asyncio 的地基(03 章)就建在 send 之上。

01 章的生成器是「单行道」:函数往外吐值,调用方只会要。这一章把另外三个方向补齐——往里塞值、往里扔异常、要求关闭——然后学 yield from 这个委托语法。这些不是炫技语法,asyncio 的地基(03 章)就建在 send 之上。


一、yield 是一个「表达式」,不只是语句

先换一个视角。01 章里 yield 都当语句用(yield 1 然后不管了)。但严格说,yield 是表达式,它有值:

x = yield 1     # 合法!yield 产出了 1,然后「表达式本身的值」是多少?

关键问题:x 等于什么?答案是——调用方说它等于什么,它就等于什么。这个「调用方说」的机制就是 send。

def echo():
    received = yield "ready"     # 产出 "ready",然后等调用方送进来一个值
    print("收到:", received)
    received2 = yield "ok"
    print("收到:", received2)

g = echo()
print(next(g))       # → 输出 ready。(必须先用 next/send(None) 启动,见下)
g.send("你好")        # → 函数内打印「收到: 你好」,继续执行到 yield "ok",返回 "ok"

逐行演算 g.send("你好"):

1. "你好" 被塞进【当前冻结的那个 yield 表达式】里
   → 上一轮冻结在 received = yield "ready" 的 yield 处
   → 现在 yield "ready" 这个表达式的值 = "你好"
   → received = "你好" 赋值完成
2. 函数继续执行:print("收到: 你好")
3. 执行到 yield "ok" → 冻结,"ok" 作为 send 的返回值

两个必须记住的规则:

  1. 刚创建的生成器必须先 next(g)(或 g.send(None))启动——因为还没冻结在任何 yield 上,没有「 yield 表达式」可以接收值。对一个新生成器直接 send("x") 会报 TypeError: can't send non-None value to a just-started generator。
  2. send 的返回值 = 生成器接下来 yield 的东西。send 既送了值进去,也拿值出来。

1.1 它有什么用?——协程的历史形态

「函数可以暂停、可以被塞值、可以恢复」——这正是协程的定义。Python 3.4 时代的 asyncio 就是用 @asyncio.coroutine + yield from 写的,await 关键字是它的语法糖后裔。现代 Web 框架里 set_user_id() / get_user_id() 这种「往当前任务里塞上下文、再取出来」的模式,语言层的原型就是 send 通道(实现换成了 ContextVar,见拔高课)。


二、throw:在暂停点注入异常

def robust():
    while True:
        try:
            item = yield "working"
        except ValueError as e:
            print("内部处理了:", e)
            continue

g = robust()
next(g)                 # 启动,冻结在 yield "working"
g.throw(ValueError("炸一下"))   # → 输出「内部处理了: 炸一下」,继续循环,返回 "working"

throw 把异常扔到生成器当前冻结的 yield 处,生成器可以在里面 try/except 接住。接不住就往外抛,生成器终止。

用得少,但它是理解「生成器是双向的」拼图的一角,也是框架代码里「从外部取消内部执行」的老式手段。


三、close 与 GeneratorExit:优雅地关掉一个生成器

01 章留了个尾巴:for 循环中途 break,生成器后来怎么收场?答案现在揭晓:

def resource_user():
    handle = open("data.txt")      # 打开了资源
    try:
        for line in handle:
            yield line.strip()
    finally:
        handle.close()             # ← 无论怎么退出都会执行
        print("资源已释放")

g = resource_user()
next(g)          # 拿第一行
g.close()        # 显式关闭
# → 输出「资源已释放」

close() 做的事:在冻结的 yield 处抛出 GeneratorExit 异常。这个异常会沿着生成器函数体的 try/finally 传播,所以 finally 里的清理代码会执行。

三个细节:

  1. GeneratorExit 继承自 BaseException 而不是 Exception——故意设计的,防止你随手写一个 except Exception 把「请关闭」的信号吞掉;
  2. 生成器在收到 GeneratorExit 后不能再 yield,否则报 RuntimeError: generator ignored GeneratorExit。「关闭时还想再吐一个」是不允许的;
  3. GC 兜底:生成器对象被垃圾回收时,Python 会自动调 close()。所以 for 循环 break 后你以为「没管它」,实际在某个时刻它被静默清理了。但依赖 GC 兜底是坏习惯——时机不可控,显式 close 或用 with 包一层才是工程做法。

这一节在流式系统里的意义:典型 SSE 生成器(04 章拆)里客户端断开 → 生成器被 close → 内部的清理逻辑(如有)通过 try/finally 执行。长连接资源释放的正确姿势,语言层就是 close/GeneratorExit 这套机制。


四、yield from:把管道接起来

手写「嵌套生成器」很啰嗦:

def inner():
    yield 1
    yield 2

def outer():
    for x in inner():      # 手动搬运
        yield x
    yield 3

yield from 一行搞定:

def outer():
    yield from inner()     # ← 委托:inner 产出的每一个值都直接透传给 outer 的调用方
    yield 3

但它不只是 for 循环的语法糖,还自动转发了三样东西:

转发什么 手写 for 版 yield from 版
产出的值 要手动 yield x 自动透传
send 进来的值 丢失(for 不转发 send) 自动转发到 inner 冻结的 yield
throw 的异常 不会进入 inner 自动注入 inner
StopIteration 的返回值 拿不到 result = yield from inner() 可以拿到

最后一行是精髓:inner 跑完时的 return 值,会成为 yield from 表达式的值:

def inner():
    yield 1
    yield 2
    return "inner 的总结"     # 生成器可以 return!值通过 StopIteration.value 传出

def outer():
    summary = yield from inner()   # inner 结束后,summary = "inner 的总结"
    print("内层返回:", summary)
    yield 3

「生产值、接收值、返回总结」三者齐全——这就是一个完整的双向协程管道。asyncio 旧版的 await 在语言层就是 yield from(await x ≈ yield from x.__await__())。你今天写 await 时省掉的复杂度,当年全靠 yield from 撑着。

4.1 实用场景:扁平化嵌套结构

def flatten(nested):
    for item in nested:
        if isinstance(item, list):
            yield from flatten(item)     # 递归委托
        else:
            yield item

list(flatten([1, [2, [3, 4]], 5]))   # → [1, 2, 3, 4, 5]

对比手写版要在两个分支里维护循环,yield from 让「委托子生成器」变成一个动作。


五、本章小结(一张表)

操作 作用 生成器侧的感受
next(g) 要下一个值 从上次暂停处继续,到下一个 yield 冻结
g.send(v) 塞一个值进去 + 要下一个值 冻结的 yield 表达式的值 = v
g.throw(exc) 在暂停点注入异常 yield 处抛出该异常,可 try/except
g.close() 请求关闭 yield 处抛 GeneratorExit,finally 执行
yield from sub 委托子生成器 值/send/throw/return 全部双向转发

↓下一步:03 章 · 异步生成器——把 yield 搬进 async def,就得到了流式链路的全部语言基础。

进入 keel 阅读