Python 的 yield from 到底转发了什么:拆开 send、throw 和那个 return 值

简介: 把生成器嵌套委托写对,关键是理解它转发的不止是产出值。本文用本机 CPython 3.13.14 实测 yield from 的 return 值透传、send/throw/close 三向内传、手写 for 循环为何丢掉 StopIteration.value、3.11 起已被移除的 YIELD_FROM 指令与 3.13 的 SEND/END_SEND 组合、PEP 479 的 RuntimeError,并给出与手写循环的八项对照表和七条排错清单

很多人把 yield from 记成「省一层 for 循环的语法糖」。写 yield from it 和写 for x in it: yield x,结果看着一样,于是就当它只是少敲几个字。

这个理解会在三个地方翻车:调用方 .send() 的值送不进去、子生成器 return 的值拿不到、异常和 close() 传不下去。yield from 真正做的事是把调用方和子生成器直接接起来,外层生成器退成一个透明通道。

本文所有输出来自本机 CPython 3.13.14 实测。

一句话结论:yield from 不是循环,是委托——next、send、throw、close 四个方向的流量全部直通子生成器,并且子生成器 return 的值会成为整个表达式的结果。

一、先看最容易被忽略的:return 值

def sub_ret():
    yield 1
    yield 2
    return 'DONE'

def outer_ret():
    r = yield from sub_ret()
    yield ('RETURNED', r)

实测:

list(outer_ret()) -> [1, 2, ('RETURNED', 'DONE')]

换成手写循环:

def manual():
    for v in sub_ret():
        yield v

list(manual()) -> [1, 2]

'DONE' 没了。for 循环通过 StopIteration 结束迭代,而 StopIteration.value 里刚好装着子生成器的返回值,只是没人去接。

yield from 接了。这是 PEP 380 设计它的主要动机之一:让生成器能像普通函数一样「返回结果」,同时保留中途 yield 的能力。asyncio 早期基于生成器的协程,正是靠这条通道把协程返回值往上传。

二、send 和 throw 是双向的

def sub_echo():
    x = yield 'READY'
    yield 'GOT:%s' % x

def outer_echo():
    yield from sub_echo()

g = outer_echo()
g.send(None)   # -> 'READY'
g.send(42)     # -> 'GOT:42'

42 穿过了 outer_echo,直接落到 sub_echo 里那个 yield 表达式的左边。外层没有写任何转发代码。

异常同理:

def sub_catch():
    try:
        yield 'A'
    except ValueError as e:
        yield 'CAUGHT:%s' % e

g2 = outer_catch()
g2.send(None)                  # -> 'A',状态 GEN_SUSPENDED
g2.throw(ValueError('boom'))   # -> 'CAUGHT:boom'

异常也是从调用方直接抛进子生成器的 yield 位置,被子生成器自己 try/except 接住。

close() 一样:外层 .close() 会让子生成器收到 GeneratorExit,子生成器的 finally 照常执行,实测子生成器打印了清理日志,随后状态变 GEN_CLOSED。

如果手写 for 循环,这四件事都得自己写一遍,还得处理 StopIteration、异常注入、GeneratorExit 的传播——这正是 PEP 380 说「样板代码既啰嗦又容易写错」的原因。

三、3.13 里,它编译成什么

反编译 outer_ret:

['RETURN_GENERATOR', 'POP_TOP', 'RESUME', 'LOAD_GLOBAL', 'CALL',
 'GET_YIELD_FROM_ITER', 'LOAD_CONST', 'SEND', 'YIELD_VALUE', 'RESUME',
 'JUMP_BACKWARD_NO_INTERRUPT', 'END_SEND', 'STORE_FAST', ...]

两个要点:

  1. 没有 YIELD_FROM 指令了。CPython 3.11 起,这条专用指令被拆成 SEND + YIELD_VALUE + END_SEND 的组合,配合 JUMP_BACKWARD_NO_INTERRUPT 组成委托循环。网上不少老教程还在讲 YIELD_FROM 字节码,在 3.13 上已经对不上。
  2. GET_YIELD_FROM_ITER 会先对对象取迭代器,所以 yield from 'abc' 合法(实测 ['a','b','c']),但对普通生成器它还要求是可迭代对象,传一个整数会直接 TypeError。

END_SEND + STORE_FAST 那一对,就是上面说的「把 StopIteration.value 存进 r」。

顺手测了性能,避免误以为语法糖有加速:

yield from range(1000) / for 循环 yield  2000 轮:0.1315s / 0.1307s,比值 0.99

几乎一样。选 yield from 的理由是语义正确,不是更快。

四、一个必须记住的坑:PEP 479

def bad():
    yield 1
    raise StopIteration('x')

list(bad())  # RuntimeError: generator raised StopIteration

从 Python 3.7 起(PEP 479),生成器里意外冒出的 StopIteration 会被改成 RuntimeError。原因是它会伪装成「迭代正常结束」,让 yield from 提前收工、静默丢数据。

这条规则和 yield from 是一套设计:委托循环靠 StopIteration 判断子生成器结束,所以这个信号不能再被业务代码随便抛。要提前结束生成器,用 return。

五、对照表

行为 yield from sub() for v in sub(): yield v
产出值 透传 透传
子生成器 return 值 作为表达式结果可接住 丢失
.send(v) 直达子生成器 yield 表达式 外层收到,需手写转发
.throw(e) 抛进子生成器 yield 位置 抛在外层
.close() / finally 传播到子生成器 不自动传播
非迭代对象 GET_YIELD_FROM_ITER 报 TypeError for 报 TypeError
3.13 字节码 GET_YIELD_FROM_ITER+SEND+YIELD_VALUE+END_SEND GET_ITER+FOR_ITER
性能 实测比值 0.99,基本持平 —

六、七条排错清单

  1. 子生成器 return 的值没拿到:检查是不是写成了 yield from sub() 却没赋值给变量;要写 r = yield from sub()。
  2. .send() 的值像石沉大海:外层多半用了手写 for 循环,改成 yield from。
  3. RuntimeError: generator raised StopIteration:业务代码里手动 raise StopIteration 了,改成 return(PEP 479)。
  4. TypeError: cannot 'yield from' a non-iterable:yield from 后面必须是可迭代对象;想推数据用 yield,不是 yield from。
  5. 反汇编看不到 YIELD_FROM:3.11+ 已改为 SEND/YIELD_VALUE/END_SEND,别照着旧资料对字节码。
  6. close() 后子生成器资源没释放:确认外层确实走了 yield from;手写循环不会替你传播 GeneratorExit。
  7. 协程里混用 yield from 和 await:async def 里委托协程要用 await,yield from 只能用于生成器/可迭代对象。

小结

yield from 把四件事一次做全:值向外、值向内、异常向内、关闭信号向内,再把子生成器的 return 值交回外层。少写一行 for 只是副作用。

判断要不要用它,问一句就够了:调用方需不需要和里层那个生成器直接对话? 需要,就用 yield from;只是想把序列摊平,普通的 for 循环也没问题。

本文代码与字节码输出均在本机 CPython 3.13.14 实测,脚本 d60-probe.py;规范依据 PEP 380、PEP 479。

相关文章
|
15天前
|
Linux 开发工具 iOS开发
Codex保姆级教程(下载+安装+登录+使用,含所有版本,2026最新)
Codex是OpenAI推出的开源AI编程智能体,本地运行,可理解整个代码库,支持自然语言指令改代码、执行命令、跑测试、修Bug,兼容Win/macOS/Linux。(239字)
6423 45
|
3天前
|
云安全 人工智能 安全
|
15天前
|
JavaScript API iOS开发
DeepSeek Harness下载、安装、配置一篇搞定(2026整理,送常用插件)
DeepSeek Harness(DSH)是DeepSeek于2026年开源的MIT协议AI Agent框架,支持插件化组装模型、工具、沙箱与UI,兼容主流大模型。本文提供免Node.js的桌面端安装包(Win/macOS),含详细图文教程及热门插件推荐。
5244 62
|
10天前
|
Linux 开发工具 iOS开发
2026最新codex下载安装保姆级使用教程(下载+安装+登录+使用,含所有历史版本)
本文详解OpenAI Codex国内快速上手指南:提供Win/macOS/Linux全平台免安装包、npm/Homebrew等多方式安装、账号/API Key双登录、简体中文切换,覆盖下载、安装、汉化、使用及高频避坑(如微软商店失败、npm超时等),10分钟即可跑通。
2586 6
|
2天前
|
JSON 前端开发 API
阿里云最新Qwen4-Max大模型介绍:模型定位、特征、能力、适用场景解析
2026 年 11 月 11 日,阿里发布第四代旗舰模型Qwen4-Max预览版,首发千问 AI 平台 Token Plan,并同步接入千问办公、Qoder 等入口。该版本聚焦知识理解、世界感知、编程与 Agentic 四项升级,主打复杂编程、专业办公、前端设计与专业软件操作。预览期至 11 月 25 日 24:00,到期后模型标识下线;限时 2 折、新用户再享 5 折,最低 1 折可体验。
|
8天前
|
人工智能 安全 API
DeepSeek Harness下载、安装、配置一篇搞定(2026整理,送常用插件)
DeepSeek Harness(dsh)是DeepSeek AI于2026年开源的轻量级Agent运行框架,基于Cordis内核,践行“一切皆插件”理念。支持Web/桌面双端,开箱即用;内置多模型接入、丰富插件生态(超3.5万),适配编程、办公、创作等场景。(239字)
1652 1
|
2天前
|
人工智能 前端开发 API
阿里云Qwen4-Max 大模型详解:定位、特征、能力、适用场景与相对Qwen3-Max差异解析
2026年11月11日,阿里发布第四代旗舰模型Qwen4-Max预览版,首发千问AI平台Token Plan并接入千问办公、Qoder等入口。该版本聚焦知识理解、世界感知、编程与Agentic四项升级,主打复杂编程、专业办公、前端设计与专业软件操作等真实任务。预览期为11月11日10:00至11月25日24:00,仅支持Token Plan调用,到期后模型标识下线;限时2折、新用户再享5折,最低1折可体验。

热门文章

最新文章