《流畅的Python》读书笔记19(第十六章:协程)

120 阅读6分钟

本章涵盖以下话题:

  • 生成器作为协程使用时的行为和状态
  • 使用装饰器自动预激协程
  • 调用方如何使用生成器对象的.close()和.throw()方法控制协程
  • 协程终止时如何返回值
  • yield from新句法的用途和语义

16.1 生成器如何进化成协程

协程是指一个过程,这个过程与调用方协作,产生由调用方提供的值。

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

示例16-1 可能是协程最简单的使用演示

>>> def simple_coroutine():

...     print('-> coroutine started')

...     x = yield

...     print('-> coroutine received:', x)

... 

>>> my_coro = simple_coroutine()

>>> my_coro

<generator object simple_coroutine at 0x7f7fc81e9db0>

>>> next(my_coro)

-> coroutine started

>>> my_coro.send(42)

-> coroutine received: 42

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

StopIteration

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

‘GEN_CREATE’

 等待开始执行

‘GEN_RUNNING’

 解释器正在执行

‘GEN_SUSPENDED’

 在yield表达式处暂停

‘GEN_CLOSED’

 执行结束

始终要先调用next(my_coro)激活协程————也可以调用my_coro.send(None)。否则会出现下述错误

>>> my_coro = simple_coroutine()

>>> my_coro.send(1729)

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

TypeError: can't send non-None value to a just-started generator

>>>

示例16-2 产出两个值协程

>>> def simple_coro2(a):

...     print('-> Started: a=', a)

...     b = yield a

...     print('-> Received: b=', a)

...     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(28)

-> Received: b= 14

42

>>> my_coro2.send(99)

-> Received: c= 99

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

StopIteration

>>> getgeneratorstate(my_coro2)

'GEN_CLOSED'

>>>

image.png 执行simple_coro2协程的3个阶段

16.3 示例:使用协程计算移动平均值

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

def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield average
        total += term
        count += 1
        average = total / count
coro_avg = averager()
next(coro_avg)
coro_avg.send(10)
10.0
coro_avg.send(30)
20.0
coro_avg.send(5)
15.0

16.4 预激协程的装饰器

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

示例16-5 预激协程的装饰器

from functools import wraps


def coroutine(func):
    @wraps(func)
    def primer(*args, **kwargs):
        gen = func(*args, **kwargs)
        next(gen)
        return gen

    return primer

示例16-6

用于计算移动平均值的协程

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

16.5 终止协程和异常处理

协程中未处理的异常会向上冒泡,传给next函数或send方法的调用方

示例16-7

coro_avg = averager()
coro_avg.send(40)
40.0
coro_avg.send(50)
45.0
coro_avg.send('spam')
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  ...
    total += term
TypeError: unsupported operand type(s) for +=: 'float' and 'str'

getgeneratorstate(coro_avg)
'GEN_CLOSED'

coro_avg.send(60) #由于在协程内没有处理异常,协程会终止。如果试图重新激活协程,会抛出StopIterator
Traceback (most recent call last):
  File "<input>", line 1, in <module>
StopIteration

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

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

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

generator.close()

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

示例16-8 coro_exc_demo.py:学习在协程中处理异常的测试代码

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

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

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'

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

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'

示例16-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):
  File "<input>", line 1, in <module>
  ...
    x = yield
ZeroDivisionError

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

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

class DemoException(Exception):
    pass


def demo_exc_handling():
    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')

16.6 让协程返回值

示例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)
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):
  File "<input>", line 1, in <module>
StopIteration: Result(count=3, average=15.5)

注意,return表达式的值会偷偷传给调用方,赋值给StopIteration异常的一个属性。

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

16.7 使用yield from

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]

示例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]

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

yield from结构的本质作用无法通过简单的可迭代对象说明,而要发散思维,使用嵌套的生成器。

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

委派生成器

 包含yield from 表达式的生成器函数

子生成器

 从yield from表达式中部分获取的生成器。

调用方

 指代调用委派生成器的客户端代码。

image.png  调用方 --> 委派生成器-->子生成器

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