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 的返回值
两个必须记住的规则:
- 刚创建的生成器必须先
next(g)(或g.send(None))启动——因为还没冻结在任何 yield 上,没有「 yield 表达式」可以接收值。对一个新生成器直接send("x")会报TypeError: can't send non-None value to a just-started generator。 - 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 里的清理代码会执行。
三个细节:
GeneratorExit继承自BaseException而不是Exception——故意设计的,防止你随手写一个except Exception把「请关闭」的信号吞掉;- 生成器在收到 GeneratorExit 后不能再 yield,否则报
RuntimeError: generator ignored GeneratorExit。「关闭时还想再吐一个」是不允许的; - 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,就得到了流式链路的全部语言基础。