Chapter 16 Coroutines
在协程中,
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 终止协程和异常处理
- 协程内部没有处理异常而有异常抛出时,协程会终止,未处理的异常会向上冒泡。因此可以通过发送哨符(
None和Ellipsis等)来终止协程。此时重新激活协程会抛出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的结果。