文章目录
- 生成器不只是省内存(下):send() 双向通信与 yield from——你的生成器也可以接收数据
- 导入语
- 1 ~> `send()`——生成器的双向通道
- 1.1 `yield` 的两种方向
- 1.2 图解 send() 的双向流动
- 1.3 第一个 `send()` 前必须先 `next()`
- 2 ~> 实战:可暂停的平均值计算器
- 3 ~> `yield from`——委托子生成器
- 3.1 什么是 `yield from`
- 3.2 双向转发的完整含义
- 3.3 双向转发场景示例
- 4 ~> `yield from` 还能处理 `return`
- 思考 && 总结
- 结尾
生成器不只是省内存(下):send() 双向通信与 yield from——你的生成器也可以接收数据
📖文章简介:上篇讲清楚了yield的暂停与恢复机制——生成器是持久化的帧对象。下篇进入两个进阶主题:send()实现了生成器的双向通信——你不仅可以"拉取"数据,也可以"推送"数据进去;yield from将子生成器的双向通信委托给父生成器——实现了类似"代理"的效果。从源码角度解释send(x)如何将 x 注入生成器的挂起位置、yield from subgen如何处理send/throw/close三种调用的转发。配有一个真实案例——用send()实现可暂停的平均值计算器,能够在运行时注入新数据并随时取出当前均值。
🎬 个人主页:源码骑士
❄专栏传送门:《Android开发基础》《python基础课程》
⭐️热衷从源码视角拆解技术底层原理,将复杂架构讲得通俗易懂
🎬 源码骑士的简介:
5年Android Framework系统开发经验,曾主导多项系统级性能优化专项
技术栈覆盖Android系统全链路(Binder/Handler/AMS/WMS/启动流程)及Java后端全家桶(Spring + MyBatis + Redis + Oracle)
累计产出原创技术文章100+篇,文章以源码拆解为特色,被读者评价为"看一篇胜过啃一周文档"
导入语
上篇我们讲完了生成器的状态机——yield暂停、next()恢复。但你有没有想过:yield不仅是"把数据送出去",它还能 “把数据接进来”?
defgen():value=yield1# ← yield 能把右边的值"送出去"……print(f"收到:{value}")# ← 也能把左边的值"接进来"这就是send()做的事情。上篇讲数据往外流,下篇讲数据往里流——双向通信。以及yield from的"代理"机制——它能把你对父生成器的操作(send/throw/close)透明地转发给子生成器。
1 ~>send()——生成器的双向通道
1.1yield的两种方向
defbidirectional():x=yield"第一次"# ← yield 右侧:送出去(返回值)print(f"收到:{x}")# ← yield 左侧:接进来(send 的参数)y=yield"第二次"print(f"收到:{y}")g=bidirectional()print(next(g))# 启动生成器,停在第一个 yield 处 → 输出 "第一次"print(g.send(100))# 把 100 推进去 → 左边 x=100 → 打印"收到: 100" → 继续到第二个 yield → 输出 "第二次"yield在这行的作用:
- 右侧:返回值给调用者(
next()或send()的返回值) - 左侧:接收调用者推送的值(
send(100)→ 赋给x)
1.2 图解 send() 的双向流动
调用者 生成器内部 g.send(100)───→[入]→ x=yield"出"───→[出]→ 返回值由调用者拿到 ↑ 这就是双向通信的位置1.3 第一个send()前必须先next()
g=bidirectional()g.send(100)# ❌ TypeError: can't send non-None value to a just-started generator生成器刚创建时停在函数开头——还没到第一个yield。send(100)想把 100 赋给yield左边的变量,但此刻没有yield可以赋值。必须先调一次next()让生成器运行到第一个yield(暂停点),然后send()才能赋值。
或者g.send(None)——等价于next(g),但只能在首次启动时用。
2 ~> 实战:可暂停的平均值计算器
defrunning_average():"""维护一个运行中的平均值——可以随时注入新值并取回当前平均值"""total=0.0count=0average=NonewhileTrue:x=yieldaverage# 暂停,等待接收新值 → 返回当前平均值ifxisnotNone:total+=x count+=1average=total/count avg_calc=running_average()next(avg_calc)# 启动生成器print(avg_calc.send(10))# 1. 当前均值... 刚好 10print(avg_calc.send(20))# 2. 当前均值... 10 + 20 → 平均值 15print(avg_calc.send(30))# 3. 当前均值... 15 + 30 → 平均值 20print(avg_calc.send(0))# 4. 平均值不变生成器在后台保留了total、count和average三个局部变量。每次send()注入新数据后,立即返回更新后的平均值。
3 ~>yield from——委托子生成器
3.1 什么是yield from
yield from subgen等效于:
foriteminsubgen:yielditem但它的真正力量不在"简化代码",而在双向转发——把调用者对这个生成器的send()、throw()和close()转发到子生成器。
3.2 双向转发的完整含义
defwrapper():yieldfromsubgen()# 等同于:defwrapper():g=subgen()result=NonewhileTrue:try:# 1. 启动/恢复子生成器 → 拿到 yield 值received=yieldresult# 2. 把外部 send() 的值转发给子生成器result=g.send(received)exceptStopIterationase:# 3. 子生成器结束了 → return 的值从 e.value 拿出break当你在父生成器上调用send(val)时:
yield from拿到val- 把它传给子生成器的
send(val) - 把子生成器的
yield值作为父生成器的返回值
3.3 双向转发场景示例
defsubgen():whileTrue:x=yieldprint(f"子生成器收到:{x}")defmain_gen():yieldfromsubgen()g=main_gen()next(g)# 启动到子生成器中的 yield 处g.send("hello")# 通过父生成器把 "hello" 转发给子生成器# 输出:子生成器收到: hello4 ~>yield from还能处理return
defsubgen_with_return():yield1yield2return"完成"defparent():result=yieldfromsubgen_with_return()print(f"子生成器返回值:{result}")g=parent()print(next(g))# 1print(next(g))# 2# 第三次 next() → 子生成器结束 → StopIteration caught → result = "完成"# → print("完成")思考 && 总结
生成器的双向通信三要点:
send(x)把值推入生成器。传入的值成为当前暂停yield左边的赋值对象。首次启动先用next()或send(None)。yield from是透明代理。send/throw/close三种调用都被转发到子生成器。子生成器的return值通过StopIteration.value传给父生成器。- 生成器的帧对象在堆中持久化——正是因为这个特性,
send()能在暂停和恢复之间传递数据。
结尾
生成器上下篇到此完结。感谢两位一路读到这里的朋友!
源码骑士 — 源码级拆解,从底层看透技术
👀关注:跟博主一起从源码视角深耕底层原理
❤️点赞:让优质内容被更多人看见
⭐收藏:核心知识点存好,随用随查
💬评论:分享你的经验或疑问,一起交流
🔄一键四连:别忘了给博主一键四连!
🗡️寄语:生成器的真核在于帧的挂起与恢复——pause & resume。
结语:生成器不是内存优化工具——它是 Python 对"可中断执行单元"的抽象。掌握它,asyncio 和异步编程才会一通百通。下一篇文章是第二板块的收官之作——手写一个迷你 Python 解释器!一键四连!