示例 16-13 是 averager 协程的不同版本,这一版会返回结果。为了说明如何返回值,每次激活协程时不会产出移动平均值。这么做是为了强调某些协程不会产出值,而是在最后返回一个值(通常是某种累计值)。

示例 16-13 中的 averager 协程返回的结果是一个 namedtuple,两个字段分别是项数(count)和平均值(average)。我本可以只返回平均值,但是返回一个元组可以获得累积数据的另一个重要信息——项数。

示例 16-13 coroaverager2.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)  ➋

➊ 为了返回值,协程必须正常终止;因此,这一版 averager 中有个条件判断,以便退出累计循环。

➋ 返回一个 namedtuple,包含 countaverage 两个字段。在 Python 3.3 之前,如果生成器返回值,解释器会报句法错误。

下面在控制台中说明如何使用新版 averager,如示例 16-14 所示。

示例 16-14 coroaverager2.py:说明 averager 行为的 doctest

    >>> coro_avg = averager()
    >>> next(coro_avg)
    >>> coro_avg.send(10)  ➊
    >>> coro_avg.send(30)
    >>> coro_avg.send(6.5)
    >>> coro_avg.send(None)  ➋
    Traceback (most recent call last):
       ...
    StopIteration: Result(count=3, average=15.5)

❶ 这一版不产出值。

❷ 发送 None 会终止循环,导致协程结束,返回结果。一如既往,生成器对象会抛出 StopIteration 异常。异常对象的 value 属性保存着返回的值。

注意,return 表达式的值会偷偷传给调用方,赋值给 StopIteration 异常的一个属性。这样做有点不合常理,但是能保留生成器对象的常规行为——耗尽时抛出 StopIteration 异常。

示例 16-15 展示如何获取协程返回的值。

示例 16-15 捕获 StopIteration 异常,获取 averager 返回的值

>>> coro_avg = averager()
>>> next(coro_avg)
>>> coro_avg.send(10)
>>> coro_avg.send(30)
>>> coro_avg.send(6.5)
>>> try:
...     coro_avg.send(None)
... except StopIteration as exc:
...     result = exc.value
...
>>> result
Result(count=3, average=15.5)

获取协程的返回值虽然要绕个圈子,但这是 PEP 380 定义的方式,当我们意识到这一点之后就说得通了:yield from 结构会在内部自动捕获 StopIteration 异常。这种处理方式与 for 循环处理 StopIteration 异常的方式一样:循环机制使用用户易于理解的方式处理异常。对 yield from 结构来说,解释器不仅会捕获 StopIteration 异常,还会把 value 属性的值变成 yield from 表达式的值。可惜,我们无法在控制台中使用交互的方式测试这种行为,因为在函数外部使用 yield from(以及 yield)会导致句法出错。4

4iPython 有个扩展——ipython-yf(https://github.com/tecki/ipython-yf),安装这个扩展后可以在 iPython 控制台中直接执行 yield from。这个扩展用于测试异步代码,可以结合 asyncio 模块使用。这个扩展已经提交为 Python 3.5 的补丁,但是没有被接受。参见 Python 缺陷追踪系统中的 22412 号工单: Towards an asyncio-enabled command line(http://bugs.python.org/issue22412)。

下一节会举例说明如何使用 yield from 结构按照 PEP 380 定义的方式获取 averager 协程返回的值。下面讨论 yield from 结构。