在协程中,yield 通常出现在表达式的右边,且可以不产出值。yield 是一种流程控制工具,利用它实现协作式多任务:协程用它把控制器让步给中心调度程序。

16.1 生成器如何进化成协程

  • 协程指一个过程,这个过程与调用方协作,产出由调用方提供的值;
  • 可以使用生成器的 .send()(发送数据)、.throw()(抛出异常到生成器中处理)、.close()(终止生成器)方法进行通信。

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

def simple_coroutine():
    print('<begin>')
    x = yield
    print('received: {}'.format(x))
    yield
    print('<end>')
    
'''
>>> coro = simple_coroutine()
>>> next(coro)
<begin>
>>> coro.send(42)
received: 42
>>> next(coro)
<end>
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
'''
  • 协程的状态可以使用 inspect.getgeneratorstate() 函数确定,包含以下可能状态:
    • GEN_CREATED:以创建实例,还未开始执行;
    • GEN_RUNNING:正在执行过程中;
    • GEN_SUSPENDED:在 yield 处暂停中;
    • GEN_CLOSED:已执行结束;
  • 协程的使用主要包括以下步骤:
    • 需要先使用 next() 函数预激(prime)协程,向前执行到第一个 yield 表达式处(也可使用 .send(None)),将产出的值作为 next() 函数的返回值(同生成器);
    • 仅当协程处于暂停状态才可使用 .send() 方法传入值,传入的值即为 yield 表达式的值(若使用 next() 函数,则相当于传入 None);
    • 传入值后,协程会继续执行到下一个 yield,将产出的值作为 .send() 方法或 next() 函数的返回值;或执行到定义体末尾,抛出异常。

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

def averager():  
    total = 0.0  
    count = 0  
    average = None  
    while True:  # <1>  
        term = yield average  # <2>  
        total += term  
        count += 1  
        average = total/count

16.4 预激协程的装饰器

  • 可以定义一个预激装饰器,用它装饰生成器函数,调用时直接返回预激后的生成器:
from functools import wraps  
  
def coroutine(func):  
    """Decorator: primes `func` by advancing to first `yield`"""  
    @wraps(func)     # 保留被装饰函数的`__doc__`和`__name__`等基本属性到新的函数中
    def primer(*args, **kwargs):     # 把被装饰的生成器函数替换为这个新的生成器函数
        gen = func(*args, **kwargs)  # 创建生成器对象
        next(gen)                    # 预激生成器
        return gen
    return primer
  • 使用 yield from 句法调用协程时,会自动预激(见16.7 节)。

16.5 终止协程和异常处理

  • 协程内部没有处理异常而有异常抛出时,协程会终止,未处理的异常会向上冒泡。因此可以通过发送哨符NoneEllipsis 等)来终止协程。此时重新激活协程会抛出 StopIteration 异常;
  • 生成器的 .throw(exc_type, [exc_value], [traceback]) 方法可以显示地在生成器的 yield 表达式处抛出指定异常。如果生成器处理了该异常,则会继续执行到下一个 yield 处并产出值,作为 .throw() 方法的返回值;否则该异常会向上冒泡;
  • .close() 方法在生成器的 yield 表达式处抛出 GeneratorExit 异常,本质上是利用生成器抛出异常会终止协程的特点来终止协程:
    • 如果生成器捕获了该异常并继续产出值,则会在调用方抛出 RuntimeError 异常(相当于尝试关闭失败);
    • 否则(没有处理这个异常,或处理异常后继续执行到生成器定义体结束而抛出 StopIteration 异常)调用方不会报错(协程已正确终止);
    • 注意:GeneratorExit 直接继承于抽象基类 BaseException 而非 Exception(根据官方文档,GeneratorExit 本质上并不是错误),因此仅使用 Exception 无法捕捉该异常;
  • 可以利用 try/finally 块的特点,用 try 包裹定义体,finally 块中加入协程结束的清理工作,这样无论如何退出协程都会执行清理工作。

16.6 让协程返回值

  • 可以在协程中使用 return 返回值。当生成器执行到 return 语句时,生成器对象抛出 StopIteration 异常,返回值会保存在异常对象的 value 属性中
try:
    coro.send(None)
except StopIteration as exc:
    result = exc.value

16.7 使用 yield from

  • yield from x 语句可对任意可迭代对象 x 调用 iter(x) 得到迭代器,然后用它产出值(该句法也能够处理一般迭代器,但其主要目的是处理生成器);
  • yield from 的主要功能是打开双向通道,把调用方和内层生成器连接起来,可以直接发送和产出值,可以直接传入异常,避免中间层的处理环节。中间层的生成器会阻塞,直到子生成器终止;
  • yield from 句法会自动捕获 StopIteration 异常,并将异常的 value 属性(返回值)作为 yield from 表达式的值
  • PEP 380 中指定的专门术语:
    • 委派生成器:包含 yield from x 表达式的生成器函数;
    • 子生成器:在 yield from x 语句中利用 x 得到的生成器;
    • 调用方:调用委派生成器的客户端代码。
from collections import namedtuple  
  
Result = namedtuple('Result', 'count average')  
  
  
# the subgenerator  
def averager():  # <1>  
    total = 0.0  
    count = 0  
    average = None  
    while True:  
        term = yield  # <2>  
        if term is None:  # <3>  
            break  
        total += term  
        count += 1  
        average = total/count  
    return Result(count, average)  # <4>  
  
  
# the delegating generator  
def grouper(results, key):  # <5>  
    while True:  # <6>  
        results[key] = yield from averager()  # <7>  

'''
# the delegating generator (in another form)
def grouper(results, key):
    results[key] = yield from averager()  # <7>
    yield
'''

  
# the client code, a.k.a. the caller  
def main(data):  # <8>  
    results = {}  
    for key, values in data.items():  
        group = grouper(results, key)  # <9>  
        next(group)  # <10>  
        for value in values:  
            group.send(value)  # <11>  
        group.send(None)  # important! <12>  
  
    print(results)

16.8 yield from 的意义

  • yield from 能够正确地传递异常和终止
    • 如果把 GeneratorExit 以外的异常传入委派生成器,那么都将调用子生成器的 .throw() 方法传入异常。子生成器处理时若抛出异常,如果是 StopIteration 则会使委派生成器恢复执行,否则会交由委派生成器捕获处理或向上冒泡;
    • 如果把 GeneratorExit 传入委派生成器(调用 .close() 方法),那么会调用子生成器的 .close() 方法,其抛出的异常向上冒泡,若无异常则委派生成器抛出 GeneratorExit(表示子生成器已正确终止);
  • 可以用以下的伪代码来理解 RESULT = yield from EXPR 的逻辑(简化版本,忽略子生成器中可能抛出的异常以及传入子生成器的异常):
_i = iter(EXPR)  # <1>  
try:  
    _y = next(_i)  # <2>  
except StopIteration as _e:  
    _r = _e.value  # <3>  
else:  
    while 1:  # <4>  
        _s = yield _y  # <5>  
        try:  
            _y = _i.send(_s)  # <6>  
        except StopIteration as _e:  # <7>  
            _r = _e.value  
            break  
  
RESULT = _r  # <8>

16.9 案例:使用协程做离散事件仿真

离散事件仿真

  • 离散事件仿真(DES):把系统建模成一系列事件的仿真类型,时间在事件之间跳转
  • 多线程的并行操作适用于实现连续仿真,协程适用于实现离散仿真(如 SimPy 库)。

出租车队运营仿真

  • 离散事件仿真的思路:将各个进程使用不同的协程表示,在主控制流程中维护事件队列和各协程的信息,每次取出一个事件,向协程中传入当前事件的持续时间,并接受协程产出的下一个事件。如果协程终止(抛出异常),则删去该协程;
  • 事件驱动型框架的运作方式:在单个线程中使用主循环驱动协程执行并发活动
  • 协程与多线程的区别:协作式多任务(自主地让出控制权)和抢占式多任务
  • 小技巧:Python 控制台中可用 _ 表示上一个非 None 的结果