yield from
是一种全新的语言结构。它的作用比 yield
多很多,以至于人们认为继续使用它会引起误解。于是在 Python3.5 中,一向以十分保守态度对待关键字的 Guido 也同意了引入关键字 async
和 await
。
关于 async
和 await
的讨论放到之后的 asyncio 中,在最新的 Python3.6 中依旧可以使用 yield from
句法。
初探 yield from
yield from
最简单的作用就是简化 for
循环中的 yield
表达式:
例如:
1 | def gen(): |
可以改写为:
1 | def gen(): |
可以看到 yield from
自动迭代了 iterable
类的对象,并且像 for
循环一样自动解决了 协程/生成器 运行结束时产生的 StopIteration
异常(重点)。
事实上,yield from x
对 x 对象所做的第一件事是:调用 iter(x)
,从中获取迭代器。因此,x 可以是任何可迭代对象(iterable)。
但是,如果 yield from
的功能仅仅是替代产出值的嵌套 for
循环,这个结构可能不会被添加到 Python 中。
yield from
的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接互相发送和产出值,还可以直接传入异常,而不用在位于中间的协程中添加大量处理异常的样板代码。有了这个结构,协程就可以通过以前不可能的方式委托职责。
为了方便说明,PEP 380 使用了一些专门的术语(请务必理解或者熟悉,后文这三个术语会大量出现)。
委派生成器
包含 yield from <iterable>
表达式的生成器函数
子生成器
从 yield from
表达式中 <iterable>
部分获取的生成器。
调用方
调用委派生成器的客户端代码。
通俗的理解就是,调用方调用委派生成器,委派生成器中有 yield from <iterable>
代码,其中 <iterable>
部分代码是可以获取的子生成器。通过委派生成器中的 yield from
,调用方和子生成器建立了联系,可以直接进行数据/异常传递。其中委派生成器 yield from
语句的值就是子生成器的返回值。
Talk is cheap. Show me the code.
下面这个示例是从字典中读取男女生体重和身高,然后将数据发送给 averager()
协程,最后生成一个报告。
1 | # example_yield_from.py |
输出:
1 | 9 boys averaging 40.42kg |
下面对上面的程序进行分析,来对 yield from
句法的用途有一个直观地认识。
还记得前面说过的调用方、委派生成器和子生成器吗?在这个程序里面,main()
函数是调用方,grouper()
函数是委派生成器,averager()
函数是子生成器。
main()
函数从 data
字典中读取数据,然后将数值部分交给委派生成器 grouper()
处理,通过 next()
函数预激委派生成器,委派生成器在 yield from
处暂停,等待子生成器返回数据。此时,委派生成器变成了 main()
函数和 averager()
函数的通道,两者可以不经过委派生成器直接互传数据。可以看到的是,在 main()
函数中,直接通过 .send()
向 averager()
发送数据而没有通过委派生成器 grouper()
。
委派生成器 grouper()
的 yield from
语句会接收子生成器 averager()
返回的值,存入 results
字典中的 key
变量所指的键中,然后委派生成器中的循环进入下一步,并在 yield from
处再次暂停,等待子生成器返回的值。所以,如果在 main()
函数中没有最后发送 None
的话(group.send(None)
),子生成器 averager()
就不会停止并返回值,yield from
最终得到的结果会是空的。
当一个协程丢失引用时(在本例中是 for
循环的一次循环执行完毕),会被垃圾回收程序回收。
yield from 的实现
通过阅读代码来理解 yield from
的实现应该是最快的。
下面列出的代码是委派生成器中下面一行代码的扩充(伪代码实现):
1 | RESULT = yielf from EXPR |
1 | _i = iter(EXPR) |
其中:
_i 是迭代器,即子生成器
_y 是子生成器产出的值
_r 是最终的结果,即 yield from
表达式的值
_s 是调用方发送给子生成器的值,会转发给子生成器
_e 是异常对象,伪代码中始终是 StopIteration 实例
伪代码首先对子生成器进行了预激,并将结果保存在 _y
中,作为产出的第一个值。
然后是一个无限循环,首先产出 _y
发送给调用方。
然后接收调用方发送的值 _s
,并将 _s
发送给子生成器(这里使用 _i.send(_s)
),这里是调用方和子生成器直接进行通信的关键,调用方发送(通过 .send()
函数)给委托生成器的数据(_s
) 被直接转发给了子生成器。
然后处理可能产生的 StopIteration
异常,子生成器返回的值就被包装在该异常中。
至于完整版的伪代码这里就不分析了,完整版的多了几个异常处理,并且区分了 _s
是否为 None 的情况。
正如伪代码中所述,yield from
会自动预激子生成器,所以上一篇提到的用于自动预激的装饰器与 yield from
结构不兼容。
通过使用 yield from
可以有效避免类似 js 中回调地狱的情况,所有的异步调用都在同一个函数中,并且异常处理也变得十分简便。
yield from
的大量使用是基于 asyncio
模块的异步编程,因此需要有有效的时间循环才能运行。下一篇会介绍 asyncio
模块的知识以及使用。