首先要知道,yield from 是全新的语言结构。它的作用比 yield 多很多,因此人们认为继续使用那个关键字多少会引起误解。在其他语言中,类似的结构使用 await 关键字,这个名称好多了,因为它传达了至关重要的一点:在生成器 gen 中使用 yield from subgen() 时,subgen 会获得控制权,把产出的值传给 gen 的调用方,即调用方可以直接控制 subgen。与此同时,gen 会阻塞,等待 subgen 终止。5

5写作本书时,有个 PEP 正在讨论中,提议增加 awaitasync 关键字:PEP 492—Coroutines with async and await syntax(https://www.python.org/dev/peps/pep-0492/)。

第 14 章说过,yield from 可用于简化 for 循环中的 yield 表达式。例如:

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

可以改写为:

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

14.10 节首次提到 yield from 时举了一个例子,演示这个结构的用法,如示例 16-16 所示。6

6示例 16-16 仅供教学使用。itertools 模块提供了优化版 chain 函数,使用 C 语言编写。

示例 16-16 使用 yield from 链接可迭代的对象

>>> def chain(*iterables):
...     for it in iterables:
...         yield from it
...
>>> s = 'ABC'
>>> t = tuple(range(3))
>>> list(chain(s, t))
['A', 'B', 'C', 0, 1, 2]

在 Beazley 与 Jones 的《Python Cookbook(第 3 版)中文版》一书中,“4.14 扁平化处理嵌套型的序列”一节有个稍微复杂(不过更有用)的 yield from 示例(源码在 GitHub 中,https://github.com/dabeaz/python-cookbook/blob/master/src/4/how_to_flatten_a_nested_sequence/example.py)。

yield from x 表达式对 x 对象所做的第一件事是,调用 iter(x),从中获取迭代器。因此,x 可以是任何可迭代的对象。

可是,如果 yield from 结构唯一的作用是替代产出值的嵌套 for 循环,这个结构很有可能不会添加到 Python 语言中。yield from 结构的本质作用无法通过简单的可迭代对象说明,而要发散思维,使用嵌套的生成器。因此,引入 yield from 结构的 PEP 380 才起了“Syntax for Delegating to a Subgenerator”(“把职责委托给子生成器的句法”)这个标题。

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__sendclosethrow 方法的生成器。

示例 16-17 能更好地说明 yield from 结构的用法。图 16-2 把该示例中各个相关的部分标识出来了。7

7图 16-2 的灵感来自 Paul Sokolovsky 绘制的示意图(http://flupy.org/resources/yield-from.pdf)。

{%}

图 16-2:委派生成器在 yield from 表达式处暂停时,调用方可以直接把数据发给子生成器,子生成器再把产出的值发给调用方。子生成器返回之后,解释器会抛出 StopIteration 异常,并把返回值附加到异常对象上,此时委派生成器会恢复

coroaverager3.py 脚本从一个字典中读取虚构的七年级男女学生的体重和身高。例如, 'boys;m' 键对应于 9 个男学生的身高(单位是米),'girls;kg' 键对应于 10 个女学生的体重(单位是千克)。这个脚本把各组数据传给前面定义的 averager 协程,然后生成一个报告,如下所示:

$ python3 coroaverager3.py
 9 boys  averaging 40.42kg
 9 boys  averaging 1.39m
10 girls averaging 42.04kg
10 girls averaging 1.43m

示例 16-17 中列出的代码显然不是解决这个问题最简单的方案,但是通过实例说明了 yield from 结构的用法。这个示例的灵感来自“What's New in Python 3.3”一文(https://docs.python.org/3/whatsnew/3.3.html#pep-380)给出的例子。

示例 16-17 coroaverager3.py:使用 yield from 计算平均值并输出统计报告

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)  # 重要!  ⓬

    # 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)

❶ 与示例 16-13 中的 averager 协程一样。这里作为子生成器使用。

main 函数中的客户代码发送的各个值绑定到这里的 term 变量上。

❸ 至关重要的终止条件。如果不这么做,使用 yield from 调用这个协程的生成器会永远阻塞。

❹ 返回的 Result 会成为 grouper 函数中 yield from 表达式的值。

grouper 是委派生成器。

❻ 这个循环每次迭代时会新建一个 averager 实例;每个实例都是作为协程使用的生成器对象。

grouper 发送的每个值都会经由 yield from 处理,通过管道传给 averager 实例。grouper 会在 yield from 表达式处暂停,等待 averager 实例处理客户端发来的值。averager 实例运行完毕后,返回的值绑定到 results[key] 上。while 循环会不断创建 averager 实例,处理更多的值。

main 函数是客户端代码,用 PEP 380 定义的术语来说,是“调用方”。这是驱动一切的函数。

group 是调用 grouper 函数得到的生成器对象,传给 grouper 函数的第一个参数是 results,用于收集结果;第二个参数是某个键。group 作为协程使用。

❿ 预激 group 协程。

⓫ 把各个 value 传给 grouper。传入的值最终到达 averager 函数中 term = yield 那一行;grouper 永远不知道传入的值是什么。

⓬ 把 None 传入 grouper,导致当前的 averager 实例终止,也让 grouper 继续运行,再创建一个 averager 实例,处理下一组值。

示例 16-17 中最后一个标号前面有个注释——“重要!”,强调这行代码(group.send(None))至关重要:终止当前的 averager 实例,开始执行下一个。如果注释掉那一行,这个脚本不会输出任何报告。此时,把 main 函数靠近末尾的 print(results) 那行的注释去掉,你会发现,results 字典是空的。

 研究为何没有收集到数据,能检验自己有没有理解 yield from 结构的运作方式。本书的代码仓库中有 coroaverager3.py 脚本的代码(https://github.com/fluentpython/example-code/blob/master/16-coroutine/coroaverager3.py)。原因说明如下。

下面简要说明示例 16-17 的运作方式,还会说明把 main 函数中调用 group.send(None) 那一行代码(带有“重要!”注释的那一行)去掉会发生什么事。

 这个试验想表明的关键一点是,如果子生成器不终止,委派生成器会在 yield from 表达式处永远暂停。如果是这样,程序不会向前执行,因为 yield from(与 yield 一样)把控制权转交给客户代码(即,委派生成器的调用方)了。显然,肯定有任务无法完成。

示例 16-17 展示了 yield from 结构最简单的用法,只有一个委派生成器和一个子生成器。因为委派生成器相当于管道,所以可以把任意数量个委派生成器连接在一起:一个委派生成器使用 yield from 调用一个子生成器,而那个子生成器本身也是委派生成器,使用 yield from 调用另一个子生成器,以此类推。最终,这个链条要以一个只使用 yield 表达式的简单生成器结束;不过,也能以任何可迭代的对象结束,如示例 16-16 所示。

任何 yield from 链条都必须由客户驱动,在最外层委派生成器上调用 next(...) 函数或 .send(...) 方法。可以隐式调用,例如使用 for 循环。

下面综述 PEP 380 对 yield from 结构的正式说明。