当前位置: 代码迷 >> 综合 >> Fluent Python - Part16 协程
  详细解决方案

Fluent Python - Part16 协程

热度:92   发布时间:2023-10-21 05:16:37.0

本章涵盖一下话题:

  • 生成器作为协程使用时的行为和状态
  • 使用装饰器自动预激协程
  • 调用方如何使用生成器的 .close().throw(...) 方法控制协程
  • 协程终止时如何返回值
  • yield from 新句法的用途和语义
  • 使用案例 — 使用协程管理仿真系统中的并发活动

生成器如何进化成协程

协程的底层架构在”PEP 342—Coroutines via Enhanced Generators”(https://www.python.org/dev/peps/pep-0342/“
) 中定义,并在 Python2.5 (2006年)实现了。自此之后, yield 关键字可以在表达式中使用,而且生成器 API 中增加了 .send(value) 方法。生成器的调用方可以使用 .send(...) 方法发送数据,发送的数据会成为生成器函数中 yield 表达式中的值。因此,生成器可以作为协程使用。协程是指一个过程,这个过程与调用方协作,产出由调用方提供的值。

除了 .send(...) 方法,PEP 342 还添加了 .throw(...).close() 方法:前者的作用是让调用方抛出异常,在生成器中处理;后者的作用是终止生成器。

PEP 380 对生成器函数的句法做了两处改动,以便更好地作为协程使用。

  • 现在,生成器可以返回一个值;以前,如果在生成器中给 return 语句提供值,会抛出 SyntaxError 异常。
  • 新引入了 yield from 句法,使用它可以把复杂的生成器重构成小型的嵌套生成器,省去了之前把生成器的工作委托给子生成器所需的大量样板代码。

用作协程的生成器的基本行为

>>> def simple_coroutine():
...     print('-> coroutine started')
...     x = yield
...     print('-> coroutine received:', x)
...
>>> my_core = simple_coroutine()
>>> my_core
<generator object simple_coroutine at 0x10f1c0dd0>
>>> next(my_core)
-> coroutine started
>>> my_core.send(42)
-> coroutine received: 42
Traceback (most recent call last):File "<stdin>", line 1, in <module>
StopIteration

协程可以身处四个状态中的一个。当前状态可以使用 inspect.getgeneratorstate(...) 函数确定,该函数会返回下述字符串中的一个。

  • GEN_CREATED: 等待开始执行。
  • GEN_RUNNING: 解释器正在执行。(只有在多线程应用中才能看到这个状态。此外,生成器对象在自己身上调用 getgeneratorstate 函数也行,不过这样做没什么用)
  • GEN_SUSPENDED: 在 yield 表达式处暂停。
  • GEN_CLOSED: 执行结束。

因为 send 方法的参数会成为暂停的 yield 表达式的值,所以,仅当协程处于暂停状态时才能调用 send 方法。

如果创建协程对象后立即把 None 之外的值发给它,会出现下述错误:

>>> my_core = simple_coroutine()
>>> my_core.send(123)
Traceback (most recent call last):File "<stdin>", line 1, in <module>
TypeError: can't send non-None value to a just-started generator

最先调用 next(my_core) 函数这一步通常称为”预激“(prime)协程(即,让协程向前执行到第一个 yield 表达式,准备好作为活跃的协程使用)

下面举个产出多个值的例子,以便更好地理解协程的行为。

>>> def simple_coro2(a):
...     print('-> Started: a=', a)
...     b = yield a
...     print('-> Received: b=', b)
...     c = yield a + b
...     print('-> Received: c=', c)
...
>>> my_coro2 = simple_coro2(14)
>>> from inspect import getgeneratorstate
>>> getgeneratorstate(my_coro2)
'GEN_CREATED'
>>> next(my_coro2)
-> Started: a= 14
14
>>> getgeneratorstate(my_coro2)
'GEN_SUSPENDED'
>>> my_coro2.send(44)
-> Received: b= 44
58
>>> my_coro2.send(12)
-> Received: c= 12
Traceback (most recent call last):File "<stdin>", line 1, in <module>
StopIteration
>>> getgeneratorstate(my_coro2)
'GEN_CLOSED'

预激协程的装饰器

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

>>> from functools import wraps
>>>
>>> def coroutine(func):
...     @wraps(func)
...     def primer(*args, **kwargs):
...             gen = func(*args, **kwargs)
...             next(gen)
...             return gen
...     return primer

使用 yield from 句法调用协程时,会自动预激,因此与 @coroutine 等装饰器不兼容。Python 3.4 标准库里的 asyncio.coroutine 装饰器不会预激协程,因此能兼容 yield from 句法。

接下来探讨协程的重要特性 — 用于终止协程,以及在协程中抛出异常的方法。

终止协程和异常处理

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

>>> def averager():
...     total = 0.0
...     count = 0
...     average = None
...     while True:
...             term = yield average
...             total += term
...             count += 1
...             average = total / count
...
>>> coro_avg = averager()
>>> coro_avg
<generator object averager at 0x10f1c0e40>
>>> next(coro_avg)
>>> coro_avg.send(40)
40.0
>>> coro_avg.send(50)
45.0
>>> coro_avg.send('apam')
Traceback (most recent call last):File "<stdin>", line 1, in <module>File "<stdin>", line 7, in averager
TypeError: unsupported operand type(s) for +=: 'float' and 'str'

出错的原因是,发送给协程的 apam 值不能加到 total 变量上。

从 Python2.5 开始,客户代码可以在生成器对象上调用两个方法,显示地把异常发给协程。

这两个方法是 throwclose

generator.throw(exc_type[, exc_value[, traceback]])

致使生成器在暂停的 `yield` 表达式处抛出指定的异常。如果生成器处理了抛出的异常,代码会向前执行到下一个 `yield` 表达式,而产出的值会成为调用 `generator.throw` 方法得到的返回值。如果生成器没有处理抛出的异常,异常会向上冒泡,传到调用方的上下文中。

generator.close()

致使生成器在暂停的 `yield` 表达式处抛出 `GeneratorExit` 异常。如果生成器没有处理这个异常,或者抛出了 `StopIteration` 异常(通常是指运行到结尾),调用方不会报错。如果收到 `GeneratorExit` 异常,**生成器一定不能产出值**, 否则解释器会抛出 `RuntimeError` 异常。生成器抛出的其他异常会向上冒泡,传给调用方。

下面举例说明如何使用 closethrow 方法控制协程。

>>> class DemoException(Exception):
...     pass
...
>>> def demo_exc_handling():
...     print('-> corouting 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.")
...
>>> exc_core = demo_exc_handling()
>>> next(exc_core)
-> corouting started
>>> exc_core.send(11)
-> coroutine received: 11
>>> exc_core.send(12)
-> coroutine received: 12
>>> exc_core.close()
>>> from inspect import getgeneratorstate
>>> getgeneratorstate(exc_core)
'GEN_CLOSED'
>>> exc_core = demo_exc_handling()
>>> next(exc_core)
-> corouting started
>>> exc_core.send(11)
-> coroutine received: 11
>>> exc_core.throw(DemoException)
*** DemoException handled. Continuing...
>>> getgeneratorstate(exc_core)
'GEN_SUSPENDED'
>>> exc_core.throw(ZeroDivisionError)
Traceback (most recent call last):File "<stdin>", line 1, in <module>File "<stdin>", line 5, in demo_exc_handling
ZeroDivisionError
>>> getgeneratorstate(exc_core)
'GEN_CLOSED'

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

class DemoException(Exception):passdef demo_finally():print('-> coroutine started')try:while True:try:x = yieldexcept DemoException:print("*** DemoException handled. Continuing...")else:print("-> coroutine received: {!r}".format(x))finally:print('-> coroutine ending')

Python3.3 引入 yield from 结构的主要原因之一与把异常传入嵌套的协程有关。另一个原因是让协程更方便地返回值。请继续往下读,了解详情。

让协程返回值

from collections import namedtupleResult = namedtuple('Result', 'count average')def averager():total = 0.0count = 0average = Nonewhile True:term = yieldif term is None:breaktotal += termcount += 1average = total/countreturn Result(count, average)coro_avg = averager()
next(coro_avg)
coro_avg.send(10)
coro_avg.send(30)
coro_avg.send(6.5)
coro_avg.send(None)""" output: Traceback (most recent call last):File "b.py", line 23, in <module>coro_avg.send(None) StopIteration: Result(count=3, average=15.5) """

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

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.valueprint(result)
""" output: Result(count=3, average=15.5) """

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

下面讨论 yield from 结构

使用 yield from

yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中添加大量处理异常的样板代码。有了这个结构,协程可以通过以前不可能的方式委托职责。

若想使用 yield from 结构,就要大幅改动代码。为了说明需要改动的部分,PEP380 使用了一些专门的术语。

委派生成器

包含 `yield from <iterable>` 表达式的生成器函数。

子生成器

从 `yield from` 表达式中 <iterable> 部分获取的生成器函数。

调用方

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

Fluent Python - Part16 协程

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

一个例子

from collections import namedtupleResult = namedtuple('Result', 'count average')# 子生成器
def averager():total = 0.0count = 0average = Nonewhile True:term = yieldif term is None:breaktotal += termcount += 1average = total/countreturn 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],
}
main(data)""" output:9 boys averaging 40.42kg9 boys averaging 1.39m 10 girls averaging 42.04kg 10 girls averaging 1.43m """

yield from 的意义

PEP380 在”Proposal“ 一节分六点说明了 yield from 的行为。

  • 子生成器产出的值都直接传给委派生成器的调用方(即客户端代码)。
  • 使用 send() 方法发给委派生成器的值都直接传给子生成器。如果发送的值是 None,那么会调用子生成器的 __next__() 方法。如果发送的值不是 None,那么会调用子生成器的 send() 方法。如果调用的方法抛出 StopIteration 异常,那么委派生成器恢复运行。任何其他异常都会向上冒泡,传给委派生成器。
  • 生成器退出时,生成器(或子生成器)中的 return expr 表达式会触发 StopIteration(expr) 异常抛出。
  • yield from表达式的值是子生成器终止时传给 StopIteration 异常的第一个参数。

yield from 结构的另外两个特性与异常和终止有关。

  • 传入委派生成器的异常,除了 GeneratorExit 之外都传给子生成器的 throw() 方法。如果调用 throw() 方法时抛出 StopIteration 异常,委派生成器恢复运行。 StopIteration 之外的异常会向上冒泡,传给委派生成器。
  • 如果把 GeneratorExit 异常传入委派生成器,或者在委派生成器上调用 close() 方法,那么在子生成器上调用 close() 方法,如果它有的话。如果调用 close() 方法导致异常抛出,那么异常会向上冒泡,传给委派生成器;否则,委派生成器抛出 GeneratorExit 异常。

奇怪,测试的时候 yield from 仍然会抛出 StopIteration,和书上介绍的不一样。。这个点回头问问别人。

下一章我们介绍”使用期物处理并发“