yield from
结构会在内部自动捕获 StopIteration
异常。这种处理方式与 for
循环处理 StopIteration
异常的方式一样:循环机制使用用户易于理解的方式处理异 常。对 yield from
结构来说,解释器不仅会捕获 StopIteration
异常,还会把 value
属性 的值变成 yield from
表达式的值。
首先要知道,yield from
是全新的语言结构。它的作用比 yield
多很多,因此人们认为继 续使用那个关键字多少会引起误解。在其他语言中,类似的结构使用 await
关键字,这个 名称好多了,因为它传达了至关重要的一点:在生成器 gen
中使用 yield from subgen()
时,subgen
会获得控制权,把产出的值传给gen
的调用方,即调用方可以直接控制 subgen
。与此同时,gen
会阻塞,等待 subgen
终止。
下面这俩函数是一样的。
yield from x
表达式对 x
对象所做的第一件事是,调用 iter(x)
,从中获取迭代器。因此,x
可以是任何可迭代的对象。
可是,如果 yield from
结构唯一的作用是替代产出值的嵌套 for
循环,这个结构很有可 能不会添加到 Python
语言中。yield from
结构的本质作用无法通过简单的可迭代对象说 明,而要发散思维,使用嵌套的生成器。
yield from
的主要功能是打开双向通道
,把最外层的调用方与最内层的子生成器连接起来, 这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中添加 大量处理异常的样板代码。有了这个结构,协程可以通过以前不可能的方式委托职责。
若想使用 yield from
结构,就要大幅改动代码。为了说明需要改动的部分,PEP 380 使用 了一些专门的术语。
委派生成器
包含 yield from <iterable>
表达式的生成器函数。
子生成器
从 yield from
表达式中<iterable>
部分获取的生成器。这就是PEP 380 的标题 (“ Syntax for Delegating to a Subgenerator”)
中所说的“子生成器”(subgenerator)
。
调用方
PEP 380 使用“调用方”这个术语指代调用委派生成器的客户端代码。在不同的语境 中,我会使用“客户端”代替“调用方”,以此与委派生成器(也是调用方,因为它调 用了子生成器)区分开。
PEP 380 经常使用“迭代器”这个词指代子生成器。这样会让人误解,因为委 派生成器也是迭代器。因此,我选择使用“子生成器”这个术语,与 PEP 380 的标题(“Syntax for Delegating to a Subgenerator”)
保持一致。然而,子生成器可 能是简单的迭代器,只实现了 __next__
方法;但是,yield from
也能处理这 种子生成器。不过,引入 yield from
结构的目的是为了支持实现了 __next__
、 send
、close
和 throw
方法的生成器。
图 16-2 把该示例中各个相关的部分标识 出来了。
下面来看例子:
"""
yield from 计算平均值并输出统计报告
"""
import pysnooper
from collections import namedtuple
Result = namedtuple('Result', 'count average')
# 子生成器
# @pysnooper.snoop()
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield
# 至关重要的终止条件。如果不这么做,
# 使用 yield from 调用这个协程的生成器会永远 阻塞。
if term is None:
break
total += term
count += 1
average = total / count
# 返回的 Result 会成为 grouper 函数中 yield from 表达式的值。
return Result(count, average)
# 委派生成器
# @pysnooper.snoop()
def grouper(results, key):
# 这个循环每次迭代时会新建一个 averager 实例;
# 每个实例都是作为协程使用的生成器 对象。
while True:
# grouper 发送的每个值都会经由yield from 处理,
# 通过管道传给averager 实例。
# grouper 会在yield from 表达式处暂停,
# 等待averager 实例处理客户端发来的值。
# averager 实例运行完毕后,返回的值绑定到 results[key] 上。
# while 循环会不断创建 averager 实例,处理更多的值
results[key] = yield from averager()
# 客户端代码,即调用方
def main(data):
results = {}
for key, values in data.items():
# group 是调用grouper 函数得到的生成器对象,
# 传给grouper 函数的第一个参数是 results,用于收集结果;
# 第二个参数是某个键。group 作为协程使用
group = grouper(results, key)
next(group)
for value in values:
# 把各个 value 传给 grouper。
# 传入的值最终到达 averager 函数中 term = yield 那一行;
# grouper 永远不知道传入的值是什么。
group.send(value)
# 把 None 传入 grouper,
# 导致当前的 averager 实例终止,
# 也让 grouper 继续运行,再创 建一个 averager 实例,处理下一组值。
group.send(None) # 重要
# print(results)
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)
运行结果:
最后一个标号前面有个注释——“重要!”,强调这行代码(group. send(None)
)至关重要:终止当前的 averager
实例,开始执行下一个。如果注释掉那一 行,这个脚本不会输出任何报告。此时,把 main
函数靠近末尾的 print(results)
那行的 注释去掉,你会发现,results
字典是空的。
下面简要说明示例 的运作方式,还会说明把 main
函数中调用 group.send(None)
那一 行代码(带有“重要!”注释的那一行)去掉会发生什么事。
1、外层for
循环每次迭代会新建一个grouper
实例,赋值给group
变量;group
是委派生成器。
2、调用 next(group)
,预激委派生成器 grouper
,此时进入 while True
循环,调用子生成 器 averager
后,在 yield from
表达式处暂停。
3、内层 for
循环调用 group.send(value)
,直接把值传给子生成器 averager
。同时,当前 的 grouper
实例(group)
在 yield from
表达式处暂停。
4、内层循环结束后,group
实例依旧在 yield from
表达式处暂停,因此,grouper
函数定 义体中为 results[key]
赋值的语句还没有执行。
5、如果外层 for
循环的末尾没有 group.send(None)
,那么 averager
子生成器永远不会终止, 委派生成器 group
永远不会再次激活,因此永远不会为 results[key]
赋值。
6、外层 for
循环重新迭代时会新建一个 grouper
实例,然后绑定到 group
变量上。前一个 grouper
实例(以及它创建的尚未终止的 averager
子生成器实例)被垃圾回收程序回收。
这个试验想表明的关键一点是,如果子生成器不终止,委派生成器会在 yield from
表达式处永远暂停。如果是这样,程序不会向前执行,因为 yield from
(与 yield
一样)把控制权转交给客户代码(即,委派生成器的调用方)了。 显然,肯定有任务无法完成。
示例 展示了 yield from
结构最简单的用法,只有一个委派生成器和一个子生成器。 因为委派生成器相当于管道,所以可以把任意数量个委派生成器连接在一起:一个委派 生成器使用 yield from
调用一个子生成器,而那个子生成器本身也是委派生成器,使用 yield from
调用另一个子生成器,以此类推。最终,这个链条要以一个只使用 yield
表达 式的简单生成器结束;不过,也能以任何可迭代的对象结束。
任何yield from
链条都必须由客户驱动,在最外层委派生成器上调用next(...)
函数 或 .send(...)
方法。可以隐式调用,例如使用 for
循环。