Python 协程中的 yield from

yield from 是一种全新的语言结构。它的作用比 yield 多很多,以至于人们认为继续使用它会引起误解。于是在 Python3.5 中,一向以十分保守态度对待关键字的 Guido 也同意了引入关键字 asyncawait

关于 asyncawait 的讨论放到之后的 asyncio 中,在最新的 Python3.6 中依旧可以使用 yield from 句法。

初探 yield from

yield from 最简单的作用就是简化 for 循环中的 yield 表达式:

例如:

1
2
3
4
5
6
7
>>> def gen():
... for c in 'AB':
... yield c
... for i in range(1, 3):
... yield i
>>> list(gen())
['A', 'B', 1, 2]

可以改写为:

1
2
3
4
5
>>> def gen():
... yield from 'AB'
... yield from range(1, 3)
>>> list(gen())
['A', 'B', 1, 2]

可以看到 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# example_yield_from.py
from collections import namedtuple

Result = namedtuple('Result', 'count average')


def averager():
total = 0.0
count = 0
average = None
while True:
term = yield
if term is None:
break
total += term
count += 1
average = total / count
return Result(count, average)


def grouper(results, key):
while True:
results[key] = yield from averager()


def main(data):
results = {}
for key, values in data.items():
group = grouper(results, key)
next(group)
for value in values:
group.send(value)
group.send(None)
report(results)


def report(results):
for key, result in sorted(results.items()):
group, unit = key.split(';')
print('{:2} {:5} averaging {:.2f}{}'.format(result.count, group, result.average, unit))


data = {
'girls;kg':
[40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5],
'girls;m':
[1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43],
'boys;kg':
[39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3],
'boys;m':
[1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46],
}


if __name__ == '__main__':
main(data)

输出:

1
2
3
4
 9 boys  averaging 40.42kg
9 boys averaging 1.39m
10 girls averaging 42.04kg
10 girls averaging 1.43m

下面对上面的程序进行分析,来对 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_i = iter(EXPR)
try:
_y = next(_i)
except StopIteration as _e:
_r = _e.value
else:
while 1:
_s = yield _y
try:
_y = _i.send(_s)
except StopIteration as _e:
_r = _e.value
break

RESULT = _r

其中:
_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 模块的知识以及使用。

-------------本文结束感谢阅读-------------
  • 本文标题:Python 协程中的 yield from
  • 本文作者:xlui
  • 发布时间:2017年10月15日 - 21:10
  • 最后更新:2023年01月20日 - 01:01
  • 本文链接: https://xlui.me/t/python-yield-from/
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明出处!