协程
yield 含义
对Python生成器中yield来说,有两层含义:产出和让步。
yield item会产出一个值,提供给next(...)的调用方;还会做出让步,暂停执行生成器,让调用方继续工作,直到需要使用另一个值时再调用next(...)。
从句法上看,协程和生成器类似,都是定义体中包含yield关键字的函数。可是,在协程中yield通常出现在表达式的右边(data = yield),可以产出值,也可以不产出。
如果yield关键字后面没有表达式,那么生成器产出None。协程可以从调用方接受数据,通过使用.send(data)方法。
yield关键字甚至还可以不接收或传出数据。不管数据如何流动,yield都是一种流程控制工具,使用它可以实现协作式多任务;
协程可以把控制器让步给中心调度程序,从而激活其他的协程。
从根本上把yield视作控制流程的方式,这样就好理解协程了。
一、用作协程的生成器的基本行为
>>> def simple_coroutine(): ... print('-> coroutine started') ... x = yield ... print('-> coroutine received:',x) ... >>> my_coro = simple_coroutine() >>> my_coro <generator object simple_coroutine at 0x000001881C76DE08> >>> next(my_coro) -> coroutine started >>> my_coro.send(998) -> coroutine received: 998 Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
协程可以身处四个状态中的一个。当前状态可以用inspect.getgeneratorstate(...)函数确定。该函数会返回下述字符串中的一个。
(1)‘GEN_CREATED’,等待开始执行
(2)‘GEN_RUNNING’,解释器正在执行
(3)‘GEN_SUSPENDED’,在yield表达式处暂停
(4)‘GEN_CLOSED’,执行结束
▲ 仅当协程处于暂停状态时才能调用send(...)方法,因此始终要调用next(my_coro)激活协程,也可以调用my_coro.send(None),效果一样。
最先调用next(...)函数这一步通常称为‘预激’(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(28) -> Received: b = 28 42 >>> my_coro2.send(99) -> Received: c = 99 Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
协程在yield关键字所在的位置暂停执行。在赋值语句中,=右边的代码在赋值之前执行。
因此,对于b=yield a 这行代码来说,等到客户端代码再次激活协程时,才会设定b的值。
运行流程:
(1)调用next(my_coro2),打印第一个消息,然后执行yield a,产出数字14。
(2)调用my_coro2.send(28),打印第二个消息,然后执行yield a+b,产出数字42.
(3)调用my_coro2.send(99),打印第三个消息,协程终止。
二、使用协程计算移动平均值
def simple_averager(): total = 0.0 count = 0 average = None while True: term = yield average count += 1 total += term average = total/count >>> coro_avg = simple_averager() >>> next(coro_avg) >>> coro_avg.send(10) 10.0 >>> coro_avg.send(30) 20.0 >>> coro_avg.send(5) 15.0
▲ 调用next(coro_avg)函数后,协程会向前执行到yield表达式,产出average变量的初始值None,因此不会出现在控制台中。
三、预激协程的装饰器
使用协程之前必须预激,可以在协程上使用一个特殊的装饰器。
from functools import wraps def coroutine(func): @wraps(func) def prime(*args,**kwargs): gen = func(*args,**kwargs) next(gen) return gen return prime @coroutine def simple_averager(): total = 0.0 count = 0 average = None while True: term = yield average count += 1 total += term average = total/count >>> coro_avg = simple_averager() >>> from inspect import getgeneratorstate >>> getgeneratorstate(coro_avg) 'GEN_SUSPENDED'
▲ 使用yield from句法调用协程时,会自动预激,因此与@coroutine等装饰器不兼容。
▲ 使用asyncio.coroutine装饰器不会预激协程,因此能兼容yield from句法。
四、终止协程和异常处理
协程中未处理的异常会向上冒泡,传给next函数或send方法的调用方(即触发协程的对象)。
终止协程的一种方式:发送某个哨符值,让协程退出。内置的None和Ellipsis等常量经常用作哨符值。也可以使用StopIteration类。如(my_coro.send(StopIteration))
Python2.5开始新增两个方法:
generator.throw(exc_type[, exc_value[, traceback]),使生成器在暂停的yield表达式处抛出指定的异常。
如果生成器处理了抛出的异常,代码会向前直行道下一个yield表达式,而产出的值会称为调用generator.throw方法得到的返回值。
generator.close(),使生成器在暂停的yield表达式处抛出GeneratorExit异常。
class DemoException(Exception): '''''' 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...')
▲ 如果传入协程的异常没有处理,协程会停止,状态变成'GEN_CLOSED'。
▲ 不管协程如何结束都想做些清理工作,要把协程定义体中相关的代码放入try/finally块中。
五、让协程返回值
为了返回值,协程必须正常终止。
例子中加入了判断条件,以便退出循环。
from collections import namedtuple Result = namedtuple('Result','count average') def simple_averager(): total = 0.0 count = 0 average = None while True: term = yield if term is None: break count += 1 total += term average = total/count return Result(count,average)
发送None会终止循环,导致协程结束,生成器对象抛出StopIteration异常,返回return的结果值。
▲ return表达式的值会传给调用方,赋值给StopIteration异常的一个属性。
try: coro_avg.send(None) except StopIteration as exc: result = exc.value
获取协程的返回值虽然要绕个圈,但这是PEP380定义的方法。
yield from结构 会在内部自动捕获StopIteration异常。与for循环处理StopIteration异常的方式一样。
六、使用 yield from
yield from是全新的语言结构,作用比yield多很多,在其他语言中,类似的结构使用await关键字。
在生成器gen中使用yield from subgen()时,subgen会获得控制权,把产出的值传给gen的调用方,即调用方可以直接控制subgen。于此同时,gen会阻塞,等待subgen终止。
yield from主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来。
这样二者可以直接发送和产出值,还可以直接传入异常,而不用在中间的协程中添加大量处理异常的样板代码。
委派生成器:包含 yield from <iterable>表达式的生成器函数
子生成器:从yield from <iterable>部分获取的生成器。
调用方:指调用委派生成器的客户端代码。
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 count += 1 total += term average = total/count return Result(count,average) def grouper(result, key): #委派生成器 while True: result[key] = yield from averager() def main(data): #调用方 result = {} for key,values in data.items(): group = grouper(result,key) next(group) for value in values: group.send(value) group.send(None) report(result) def report(Result): for key,values in Result.items(): print(key,values) data = { 'girls;kg': [40.9,23,56,78,48,52,38.0], } main(data)
运行方式:
(1)外层for循环每次迭代会新建一个grouper实例,复制给group变量,group是委派生成器。
(2)调用next(group),预激委派生成器,此时进入while True循环,调用子生成器averager后,在yield from表达式处暂停。
(3)内层for循环调用group.send(value),直接把值传给子生成器averager。同时,当前的grouper实例(group)在yield from表达式处暂停。
(4)内层循环结束后,group实例依旧在yield from处暂停,因此,grouper函数定义体中result[key]赋值语句还没有执行。
(5)如果外层for循环的末尾没有group.send(None),那么averager子生成器永远不会终止,委派生成器group永远不会再次激活,因此永远不会为result[key]赋值。
(6)外层for循环重新迭代时会新建一个grouper实例,然后绑定到group变量上。前一个grouper实例被垃圾回收程序回收。
▲ 委派生成器相当于管道。
▲ 可以把任意数量的委派生成器连接在一起。而子生成器本身也是一个委派生成器,使用yield from调用另一个子生成器,以此类推。
▲ 最终这个链条要以一个只使用表达式yield的简单生成器结束,也可以任何可迭代的对象结束。
七、yield from的意义
子生成器产出的值都直接传给委派生成器的调用方。
使用send()方法发给委派生成器的值都直接传给子生成器。
生成器退出时,生成器(或子生成器)中的return expr表达式会触发StopIteration(expr)异常抛出。
yield from表达式的值是子生成器终止时传给StopIteration异常的第一个参数。
八、使用协程做离散事件仿真
在离散事件仿真中,仿真‘钟’向前推荐的量不是固定的,而是直接推进到下一个事件模型的模拟时间。