示例 定义一个计算移动平均值的协程

1
2
3
4
5
6
7
8
9
def averager():
    total = 0.0
    count = 0
    average = None
    while True:  
        term = yield average  
        total += term
        count += 1
        average = total/count

➊ 这个无限循环表明,只要调用方不断把值发给这个协程,它就会一直接收值,然后生 成结果。仅当调用方在协程上调用 .close() 方法,或者没有对协程的引用而被垃圾回收 程序回收时,这个协程才会终止。

➋ 这里的 yield 表达式用于暂停执行协程,把结果发给调用方;还用于接收调用方后面 发给协程的值,恢复无限循环。 使用协程的好处是,total 和 count 声明为局部变量即可,无需使用实例属性或闭包在 多次调用之间保持上下文。示例 16-4 是使用 averager 协程的 doctest。

示例 定义的移动平均值协程的 doctest

1
2
3
4
5
6
7
8
>>> coro_avg = averager()  
>>> next(coro_avg)  
>>> coro_avg.send(10)  
10.0
>>> coro_avg.send(30)
20.0
>>> coro_avg.send(5)
15.0

❶ 创建协程对象。 ❷ 调用 next 函数,预激协程。 ❸ 计算移动平均值:多次调用 .send(…) 方法,产出当前的平均值。

在上述doctest中,调用next(coro_avg)函数后,协程会向前执行到yield表达式,产出average变量的初始值——None,因此不会出现在控制台中。此时,协程在yield表达式处暂停,等到调用方发送值。coro_avg.send(10) 那一行发送一个值,激活协程,把发送的值赋给yield average表达式,也就是term,并更新total、count和average三个变量的值,然后开始while循环的下一次迭代,产出average变量的值,等待下一次为term变量赋值。

如何启动协程。使用协程之前必须预激,可是这一步容易忘记。为了避免忘记,可以在协程上使用一个特殊的装饰器。

预激协程的装饰器

如果不预激,那么协程没什么用。调用my_coro.send(x) 之前,记住一定要调用next(my_coro)。为了简化协程的用法,有时会使用一个预激装饰器。

示例 预激协程的装饰器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from functools import wraps

def coroutine(func):
"""装饰器:向前执行到第一个`yield`表达式,预激`func`"""
   @wraps(func)
   def primer(*args,**kwargs):  
       gen = func(*args,**kwargs)  
       next(gen)  
       return gen  
   return primer

❶ 把被装饰的生成器函数替换成这里的 primer 函数;调用 primer 函数时,返回预激后的生成器。
❷ 调用被装饰的函数,获取生成器对象。
❸ 预激生成器。
❹ 返回生成器。

示例 展示@coroutine 装饰器的用法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> from coroutil import coroutine  
>>> @coroutine  
>>> def averager():
...    total = 0.0
...    count = 0
...    average = None
...    while True:
...        term = yield average
...        total += term
...        count += 1
...        average = total/count
...
>>> coro_avg = averager()  
>>> from inspect import getgeneratorstate
>>> getgeneratorstate(coro_avg)  
'GEN_SUSPENDED'
>>> coro_avg.send(10)  
10.0
>>> coro_avg.send(30)
20.0
>>> coro_avg.send(5)
15.0

❶ 导入 coroutine 装饰器。
❷ 把装饰器应用到 averager 函数上。
❸ 调用 averager() 函数创建一个生成器对象,在 coroutine 装饰器的 primer 函数中 已经预激了这个生成器。
❹ getgeneratorstate 函数指明,处于 GEN_SUSPENDED 状态,因此这个协程已经准备 好,可以接收值了。
❺ 可以立即开始把值发给 coro_avg——这正是 coroutine 装饰器的目的。

终止协程和异常处理

协程中未处理的异常会向上冒泡,传给 next 函数或 send 方法的调用方(即触发协程的 对象)。

示例 未处理的异常会导致协程终止

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
>>> from coroaverager1 import averager
>>> coro_avg = averager()
>>> coro_avg.send(40)  # ➊
40.0
>>> coro_avg.send(50)
45.0
>>> coro_avg.send('spam')  # ➋
Traceback (most recent call last):
 ...
TypeError: unsupported operand type(s) for +=: 'float' and 'str'
>>> coro_avg.send(60)  # ➌
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
StopIteration

❶ 使用 @coroutine 装饰器装饰的 averager 协程,可以立即开始发送值。
❷ 发送的值不是数字,导致协程内部有异常抛出。
❸ 由于在协程内没有处理异常,协程会终止。如果试图重新激活协程,会抛出StopIteration异常。出错的原因是,发送给协程的’spam’值不能加到 total 变量上。

示例暗示了终止协程的一种方式:发送某个哨符值,让协程退出。内置的 None 和 Ellipsis 等常量经常用作哨符值。Ellipsis 的优点是,数据流中不太常有这个值。我 还见过有人把 StopIteration类(类本身,而不是实例,也不抛出)作为哨符值;也就是说,是像这样使用的:my_coro.send(StopIteration)。 从Python 2.5 开始,客户代码可以在生成器对象上调用两个方法,显式地把异常发给协程。 这两个方法是 throw 和 close。

generator.throw(exc_type[, exc_value[, traceback]])
致使生成器在暂停的 yield 表达式处抛出指定的异常。如果生成器处理了抛出的异 常,代码会向前执行到下一个 yield 表达式,而产出的值会成为调用 generator.throw 方法得到的返回值。如果生成器没有处理抛出的异常,异常会向上冒泡,传到调用方的上下文中。 generator.close()
致使生成器在暂停的 yield 表达式处抛出 GeneratorExit 异常。如果生成器没有处 理这个异常,或者抛出了 StopIteration 异常(通常是指运行到结尾),调用方不会报 错。如果收到 GeneratorExit 异常,生成器一定不能产出值,否则解释器会抛出 RuntimeError 异常。生成器抛出的其他异常会向上冒泡,传给调用方。

下面举例说明如何使用 close 和 throw 方法控制协程。 示例 coro_exc_demo.py:学习在协程中处理异常的测试代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class DemoException(Exception):
"""为这次演示定义的异常类型。"""
    pass

def demo_exc_handling():
    print('-> coroutine started')
    while True:
        try:
            x = yield
        except DemoException:  
            print('*** DemoException handled. Continuing...')
        else:  
            print('-> coroutine received: {!r}'.format(x))
    raise RuntimeError('This line should never run.')  

❶ 特别处理 DemoException 异常。
❷ 如果没有异常,那么显示接收到的值。
❸ 这一行永远不会执行。

示例中的最后一行代码不会执行,因为只有未处理的异常才会中止那个无限循环, 而一旦出现未处理的异常,协程会立即终止。

示例 激活和关闭 demo_exc_handling,没有异常

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
>>> exc_coro = demo_exc_handling()
>>> next(exc_coro)
-> coroutine started
>>> exc_coro.send(11)
-> coroutine received:
11
>>> exc_coro.send(22)
-> coroutine received: 22
>>> exc_coro.close()
>>> from inspect import getgeneratorstate
>>> getgeneratorstate(exc_coro)
'GEN_CLOSED'

如果把 DemoException 异常传入 demo_exc_handling 协程,它会处理,然后继续运行,如下面示例 所示

示例 把DemoException异常传入demo_exc_handling 不会导致协程中止

1
2
3
4
5
6
7
8
9
>>> exc_coro = demo_exc_handling()
>>> next(exc_coro)
-> coroutine started
>>> exc_coro.send(11)
-> coroutine received: 11
>>> exc_coro.throw(DemoException)
*** DemoException handled. Continuing...
>>> getgeneratorstate(exc_coro)
'GEN_SUSPENDED'

但是,如果传入协程的异常没有处理,协程会停止,即状态变成 ‘GEN_CLOSED’。下面的示例 演示了这种情况。

示例  如果无法处理传入的异常,协程会终止

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
>>> exc_coro = demo_exc_handling()
>>> next(exc_coro)
-> coroutine started
>>> exc_coro.send(11)
-> coroutine received: 11
>>> exc_coro.throw(ZeroDivisionError)
Traceback (most recent call last):
 ...
ZeroDivisionError
>>> getgeneratorstate(exc_coro)
'GEN_CLOSED'

如果不管协程如何结束都想做些清理工作,要把协程定义体中相关的代码放入 try/finally 块中,如示例。

示例 coro_finally_demo.py:使用 try/finally 块在协程终止时执行操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class DemoException(Exception):
"""为这次演示定义的异常类型。"""

def demo_finally():
    print('-> coroutine started')
    try:
        while True:
            try:
                x = yield
            except DemoException:
                print('*** DemoException handled. Continuing...')
            else:
                print('-> coroutine received: {!r}'.format(x))
    finally:
        print('-> coroutine ending')

让协程返回值

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

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

示例 coroaverager2.py:定义一个求平均值的协程,让它返回一个结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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,包含 count 和 average 两个字段。在 Python 3.3 之前,如果生成器返回值,解释器会报句法错误。

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

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

1
2
3
4
5
6
7
8
9
>>> 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 异常。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
>>> 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。yield from 结构会在内部自动捕获 StopIteration 异常。这种处理方 式与 for 循环处理 StopIteration 异常的方式一样:循环机制使用用户易于理解的方式处理异常。对 yield from 结构来说,解释器不仅会捕获 StopIteration 异常,还会把 value属性的值变成yield from表达式的值。可惜,我们无法在控制台中使用交互的方 式测试这种行为,因为在函数外部使用 yield from(以及 yield)会导致句法出错。

Python 3.3 引入 yield from结构的主要原因之一与把异常传入嵌套的协程有关。另一个原因是让协程更方便地返回值。

使用yield from

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

yield from 可用于简化 for 循环中的 yield 表达式。例如:

1
2
3
4
5
6
7
8
>>> 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
6
>>> def gen():
 ...     yield from 'AB'
 ...     yield from range(1, 3)
 ...
>>> list(gen())
['A', 'B', 1, 2

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 使用“调用方”这个术语指代调用委派生成器的客户端代码。在不同的语境 中,我会使用“客户端”代替“调用方”,以此与委派生成器(也是调用方,因为它调用了子 生成器)区分开。

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

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

❶ 与示例 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 实例,处理下一组值。
下面简要说明示例的运作方式,还会说明把 main 函数中调用 group.send(None) 那一行代码(带有“重要!”注释的那一行)去掉会发生什么事。

.外层for循环每次迭代会新建一个grouper实例,赋值给group变量;group 是委派生成器。
.调用next(group),预激委派生成器grouper,此时进入while True循环,调用子生成器averager后,在 yield from 表达式处暂停。
.内层for循环调用group.send(value),直接把值传给子生成器averager。同时,当前的grouper实例(group)在 yield from 表达式处暂停。
.内层循环结束后,group实例依旧在yield from表达式处暂停,因此,grouper函数定义体中为results[key] 赋值的语句还没有执行。
.如果外层 for 循环的末尾没有group.send(None),那么averager子生成器永远不会终止,委派生成器group永远不会再次激活,因此永远不会为 results[key] 赋值。
.外层for循环重新迭代时会新建一个grouper实例,然后绑定到group变量上。前一个grouper实例(以及它创建的尚未终止的 averager 子生成器实例)被垃圾回收程序回收。

Python-Coroutine2.PNG

图 把该示例中各个相关的部分 标识出来了。委派生成器在 yield from 表达式处暂停时,调用方可以直接把数据发给子 生成器,子生成器再把产出的值发给调用方。子生成器返回之后,解释器会抛出 StopIteration 异常,并把返回值附加到异常对象上,此时委派生成器会恢复

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

示例展示了 yield from 结构最简单的用法,只有一个委派生成器和一个子生成 器。因为委派生成器相当于管道,所以可以把任意数量个委派生成器连接在一起:一个委派生成器使用yield from调用一个子生成器,而那个子生成器本身也是委派生成器,使用 yield from 调用另一个子生成器,以此类推。最终,这个链条要以一个只使用yield 表达式的简单生成器结束;不过,也能以任何可迭代的对象结束。 任何yield from链条都必须由客户驱动,在最外层委派生成器上调用next(…) 函数 或 .send(…) 方法。可以隐式调用,例如使用for循环。

下面综述 PEP 380 对 yield from 结构的正式说明。PEP 380 在“Proposal”一节https://www.python.org/dev/peps/pep-0380/#proposal 分六点说明了 yield from 的行为。

1)子生成器产出的值都直接传给委派生成器的调用方(即客户端代码)。

2)使用 send() 方法发给委派生成器的值都直接传给子生成器。如果发送的值是 None,那么会调用子生成器的next() 方法。如果发送的值不是 None,那么会调用子生成器的 send() 方法。如果调用的方法抛出 StopIteration 异常,那么委派生成器恢复运行。任何其他异常都会向上冒泡,传给委派生成器。

3)生成器退出时,生成器(或子生成器)中的return expr表达式会触发StopIteration(expr) 异常抛出。

4)yield from 表达式的值是子生成器终止时传给StopIteration 异常的第一个参数。

5)传入委派生成器的异常,除了GeneratorExit之外都传给子生成器的throw() 方法。如果调用throw()方法时抛出StopIteration异常,委派生成器恢复运行。StopIteration 之外的异常会向上冒泡,传给委派生成器。

6)如果把 GeneratorExit 异常传入委派生成器,或者在委派生成器上调用 close() 方法,那么在子生成器上调用close()方法,如果它有的话。如果调用close()方法导致异常抛出,那么异常会向上冒泡,传给委派生成器;否则,委派生成器抛出GeneratorExit异常。


转载请注明本网址。