若是 Python 书籍有必定的指导做用,那么(协程就是)文档最匮乏、最不为人知的 Python 特性,所以表面上看是最无用的特性。python
——David Beazley算法
Python 图书做者编程
字典为动词“to yield”给出了两个释义:产出和让步。对于 Python 生成器中的 yield 来讲,这两个含义都成立。yield item 这行代码会产出一个值,提供给 next(...) 的调用方;此外,还会做出让步,暂停执行生成器,让调用方继续工做,直到须要使用另外一个值时再调用next()。调用方会从生成器中拉取值。多线程
从句法上看,协程与生成器相似,都是定义体中包含 yield 关键字的函数。但是,在协程中,yield 一般出如今表达式的右边(例如,datum = yield),能够产出值,也能够不产出——若是 yield关键字后面没有表达式,那么生成器产出 None。协程可能会从调用方接收数据,不过调用方把数据提供给协程使用的是 .send(datum) 方法,而不是 next(...) 函数。一般,调用方会把值推送给协程。闭包
yield 关键字甚至还能够不接收或传出数据。无论数据如何流动,yield 都是一种流程控制工具,使用它能够实现协做式多任务:协程能够把控制器让步给中心调度程序,从而激活其余的协程。架构
从根本上把 yield 视做控制流程的方式,这样就好理解协程了。异步
生成器如何进化成协程异步编程
协程的底层架构在“PEP 342—Coroutines via EnhancedGenerators”(https://www.python.org/dev/peps/pep-0342/)中定义,并在Python 2.5(2006 年)实现了。自此以后,yield 关键字能够在表达式中使用,并且生成器 API 中增长了 .send(value) 方法。生成器的调用方可使用 .send(...) 方法发送数据,发送的数据会成为生成器函数中 yield 表达式的值。所以,生成器能够做为协程使用。协程是指一个过程,这个过程与调用方协做,产出由调用方提供的值。函数
除了 .send(...) 方法,PEP 342 还添加了 .throw(...) 和 .close()方法:前者的做用是让调用方抛出异常,在生成器中处理;后者的做用是终止生成器。工具
用做协程的生成器的基本行为
举个 🌰 演示协程的用法
1 def simple_coroutine(): # 携程使用生成器函数定义:定义题中有yield关键字 2 print('-> coroutine started') # 若是携程只从客户那里接受数据,那么产出的值是None,这个值是隐式的,由于yield关键字右边没有表达式 3 x = yield 4 print('-> coroutine received:', x) 5 6 my_coro = simple_coroutine() 7 print(my_coro) # 与建立生成器的方式同样,调用函数获得生成器对象 8 next(my_coro) # 首先要调用next(..)函数,由于生成器尚未启动,没在yield语句初暂停,因此一开始没法发送数据 9 10 my_coro.send(10) # 调用这个方法后,携程定义中的yield表可是会出现10,直到下一个yield出现或者终止
以上代码执行的结果为:
<generator object simple_coroutine at 0x102a463b8> -> coroutine started -> coroutine received: 10 Traceback (most recent call last): ........... my_coro.send(10) # 调用这个方法后,携程定义中的yield表可是会出现10,直到下一个yield出现或者终止 StopIteration
协程能够身处四个状态中的一个。当前状态可使用inspect.getgeneratorstate(...) 函数肯定,该函数会返回下述字符串中的一个。
'GEN_CREATED'
等待开始执行。
'GEN_RUNNING'
解释器正在执行。
'GEN_SUSPENDED'
在 yield 表达式处暂停。
'GEN_CLOSED'
执行结束。
由于 send 方法的参数会成为暂停的 yield 表达式的值,因此,仅当协程处于暂停状态时才能调用 send 方法,例如 my_coro.send(10)。不过,若是协程还没激活(即,状态是 'GEN_CREATED'),状况就不一样了。所以,始终要调用 next(my_coro) 激活协程——也能够调用my_coro.send(None),效果同样。
若是建立协程对象后当即把 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
注意错误消息,它表述得至关清楚。最早调用 next(my_coro) 函数这一步一般称为“预激”(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一下 'GEN_CREATED' >>> next(my_coro2) # 向前执行协程到第一个yield表达式,打印-> Started 这个信息之后,而后产出的a的值,而且中止,等待为b赋值 -> Started: a = 14 14 >>> getgeneratorstate(my_coro2) # 查看协程的状态,如今处于GEN_SUSPENDED状态(即协程在yield表达式处暂停) 'GEN_SUSPENDED' >>> my_coro2.send(28) # 把数字28发给暂停的协程,计算yield表达式,获得28,而后绑定给b,产出a + b的值(42),而后协程暂停,等待为c赋值 -> Received: b = 28 42 >>> my_coro2.send(99) # 把数字99发送给暂停的协程,计算yield表达式,获得99,而后把获得的数字绑定给c,而后协程终止。致使生成器抛出StopIteration -> Received: c = 99 Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration >>> getgeneratorstate(my_coro2) # 协程的状态处于GEN_CLOSED状态 'GEN_CLOSED'
关键的一点是,协程在 yield 关键字所在的位置暂停执行。前面说过,在赋值语句中,= 右边的代码在赋值以前执行。所以,对于 b =yield a 这行代码来讲,等到客户端代码再激活协程时才会设定 b 的值。这种行为要花点时间才能习惯,不过必定要理解,这样才能弄懂异步编程中 yield 的做用。
simple_coro2 协程的执行过程分为 3 个阶段,如图下图所示。
(1) 调用 next(my_coro2),打印第一个消息,而后执行 yield a,产出数字 14。
(2) 调用 my_coro2.send(28),把 28 赋值给 b,打印第二个消息,然后执行 yield a + b,产出数字 42。
(3) 调用 my_coro2.send(99),把 99 赋值给 c,打印第三个消息,协程终止。
执行 simple_coro2 协程的 3 个阶段(注意,各个阶段都在yield 表达式中结束,并且下一个阶段都从那一行代码开始,而后再把 yield 表达式的值赋给变量)
示例:使用协程计算移动平均值
使用协程的好处是,total 和 count 声明为局部变量便可,无需使用实例属性或闭包在屡次调用之间保持上下文。下面的 🌰 是使用averager 协程的 doctest。
coroaverager0.py:定义一个计算移动平均值的协程
1 def averager(): 2 total = 0 3 count = 0 4 average = None 5 while True: # 无限循环一直会不断的把值发送给这个协程,它就会一直接受,而后生成结果 6 # 仅当调用方在协程上调用.close()方法,或者没有对协程引用的时候才会终止 7 term = yield average # 这里的yield表达式用于暂停执行协程,把结果发送给调用方,还用于接受调后面发给协程的值 8 total += term 9 count += 1 10 average = total/count
以上代码执行的结果为:
>>> coro_avg = averager() # 建立协程对象 >>> next(coro_avg) # 调用next函数,预激协程 >>> coro_avg.send(10) # 计算平均值:屡次调用send(...)方法,产出当前平均值 10.0 >>> coro_avg.send(30) 20.0 >>> coro_avg.send(5) 15.0
在上述 doctest 中,调用 next(coro_avg) 函数后,协程会向前执行到 yield 表达式,产出 average 变量的初始值——None,所以不会出如今控制台中。此时,协程在 yield 表达式处暂停,等到调用方发送值。coro_avg.send(10) 那一行发送一个值,激活协程,把发送的值赋给 term,并更新 total、count 和 average 三个变量的值,而后开始 while 循环的下一次迭代,产出 average 变量的值,等待下一次为 term 变量赋值。
预激协程的装饰器
若是不预激,那么协程没什么用。调用 my_coro.send(x) 以前,记住必定要调用 next(my_coro)。为了简化协程的用法,有时会使用一个预激装饰器。
1 from functools import wraps 2 from inspect import getgeneratorstate 3 4 5 def coroutine(func): 6 @wraps(func) 7 def primer(*args, **kwargs): # 把被装饰的生成器函数天换成这里的primer函数,调用peimer函数时,返回预激后的生成器 8 gen = func(*args, **kwargs) # 获取生成器对象 9 next(gen) # 预激活 10 return gen # 返回生成器 11 return primer 12 13 14 @coroutine # 预激活装饰器 15 def averager(): 16 total = 0 17 count = 0 18 average = None 19 while True: # 无限循环一直会不断的把值发送给这个协程,它就会一直接受,而后生成结果 20 # 仅当调用方在协程上调用.close()方法,或者没有对协程引用的时候才会终止 21 term = yield average # 这里的yield表达式用于暂停执行协程,把结果发送给调用方,还用于接受调后面发给协程的值 22 total += term 23 count += 1 24 average = total/count 25 26 coro_avg = averager() # 调用averager()函数建立一个生成器对象,在coroutine装饰器的primer函数中已预激活 27 print(getgeneratorstate(coro_avg)) # 查看协程的状态,已是能够接收值得状态咯 28 print(coro_avg.send(10)) # 给协程发送数据 29 print(coro_avg.send(30)) 30 print(coro_avg.send(5))
以上代码的执行结果为:
GEN_SUSPENDED
10.0
20.0
15.0
终止协程和异常处理
协程中未处理的异常会向上冒泡,传给 next 函数或 send 方法的调用方(即触发协程的对象)
>>> from coroaverager1 import averager >>> coro_avg = averager() >>> coro_avg.send(40) # 使用@corotine装饰器装饰的averager协程,能够当即开始发送值 40.0 >>> coro_avg.send(50) 45.0 >>> coro_avg.send('spam') # 发送的值不是数字,致使协程内部有异常抛出 Traceback (most recent call last): ... TypeError: unsupported operand type(s) for +=: 'float' and 'str' >>> coro_avg.send(60) # 因为异常没有处理,so...协程会终止。若是试图从新激活协程,会抛出StopIteration异常 Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
出错的缘由是,发送给协程的 'spam' 值不能加到 total 变量上
暗示了终止协程的一种方式:发送某个哨符值,让协程退出。内置的 None 和 Ellipsis 等常量常常用做哨符值。Ellipsis 的优势是,数据流中不太常有这个值。我还见过有人把 StopIteration类(类自己,而不是实例,也不抛出)做为哨符值;也就是说,是像这样使用的:my_coro.send(StopIteration)。
从 Python 2.5 开始,客户代码能够在生成器对象上调用两个方法,显式地把异常发给协程。
这两个方法是 throw 和 close:
generator.throw(exc_type[, exc_value[, traceback]])
导致生成器在暂停的 yield 表达式处抛出指定的异常。若是生成器处理了抛出的异常,代码会向前执行到下一个 yield 表达式,而产出的值会成为调用 generator.throw 方法获得的返回值。若是生成器没有处理抛出的异常,异常会向上冒泡,传到调用方的上下文中。
generator.close()
导致生成器在暂停的 yield 表达式处抛出 GeneratorExit 异常。若是生成器没有处理这个异常,或者抛出了 StopIteration 异常(一般是指运行到结尾),调用方不会报错。若是收到 GeneratorExit 异常,生成器必定不能产出值,不然解释器会抛出 RuntimeError 异常。生成器抛出的其余异常会向上冒泡,传给调用方。
coro_exc_demo.py:学习在协程中处理异常的测试代码
1 class DemoException(Exception): 2 """为了掩饰定义的异常类型""" 3 4 def demo_exc_handling(): 5 print('-> coroutine startedd') 6 while True: 7 try: 8 x = yield 9 except DemoException: # 特别处理 DemoException 异常 10 print('*** DemoException handled. Continuing...') 11 else: # 没有异常就接收值 12 print('-> coroutine received: {!r}'.format(x)) 13 14 raise RuntimeError('This line should never run.') # while True会不中止的循环,这同样会一直执行
激活和关闭 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'
若是把 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'
可是,若是传入协程的异常没有处理,协程会中止,即状态变成'GEN_CLOSED'
>>> 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): ... ZeroDivisionError >>> getgeneratorstate(exc_coro) 'GEN_CLOSED'
若是无论协程如何结束都想作些清理工做,要把协程定义体中相关的代码放入 try/finally 块中
🌰 coro_finally_demo.py:使用 try/finally 块在协程终止时执行操做
1 class DemoException(Exception): 2 pass 3 4 def demo_finall(): 5 print('-> coroutine started') 6 try: 7 while True: 8 try: 9 x = yield 10 except DemoException: 11 print('*** DemoException handled. Continuing...') 12 else: 13 print('-> coroutine received: {!r}'.format(x)) 14 finally: 15 print('-> coroutine ending')
让协程返回值
下面的 🌰 是 averager 协程的不一样版本,这一版会返回结果。为了说明如何返回值,每次激活协程时不会产出移动平均值。这么作是为了强调某些协程不会产出值,而是在最后返回一个值(一般是某种累计值)。
1 from collections import namedtuple 2 3 Result = namedtuple('Result', 'count average') 4 5 def averager(): 6 total = 0.0 7 count = 0 8 average = None 9 while True: 10 term = yield 11 if term is None: # 当send(None)的时候,终止协程 12 break 13 total += term 14 count += 1 15 average = total/count 16 return Result(count, average) # 返回一个namedtuple,包含两个字段count, average
注意:
return 表达式的值会偷偷传给调用方,赋值给 StopIteration异常的一个属性。这样作有点不合常理,可是能保留生成器对象的常规行为——耗尽时抛出 StopIteration 异常。
演示🌰 捕获 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)
使用yield from
首先要知道,yield from 是全新的语言结构。它的做用比 yield 多不少,所以人们认为继续使用那个关键字多少会引发误解。在其余语言中,相似的结构使用 await 关键字,这个名称好多了,由于它传达了相当重要的一点:在生成器 gen 中使用 yield from subgen()时,subgen 会得到控制权,把产出的值传给 gen 的调用方,即调用方能够直接控制 subgen。与此同时,gen 会阻塞,等待 subgen 终止。
举个🌰 yield from 可用于简化 for 循环中的 yield 表达式
1 def gen(): 2 for i in 'AB': 3 yield i 4 5 for i in range(1, 3): 6 yield i 7 8 print(list(gen())) 9 10 11 ''' 12 yield from 版本,能够简化内部的for循环 13 ''' 14 def gen(): 15 yield from 'AB' 16 yield from range(1, 3) 17 18 print(list(gen()))
🌰 使用 yield from 连接可迭代的对象
1 def chain(*iterable): 2 for i in iterable: 3 yield from i 4 5 s = 'ABC' 6 t = tuple(range(1, 5)) 7 r = list(chain(s, t)) 8 print(r)
以上代码执行的结果为:
['A', 'B', 'C', 1, 2, 3, 4]
yield from x 表达式对 x 对象所作的第一件事是,调用 iter(x),从中获取迭代器。所以,x 能够是任何可迭代的对象。
yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器链接起来,这样两者能够直接发送和产出值,还能够直接传入异常,而不用在位于中间的协程中添加大量处理异常的样板代码。有了这个结构,协程能够经过之前不可能的方式委托职责。
委派生成器
包含 yield from <iterable> 表达式的生成器函数。
子生成器
从 yield from 表达式中 <iterable> 部分获取的生成器。
调用方
PEP 380 使用“调用方”这个术语指代调用委派生成器的客户端代码。在不一样的语境中,我会使用“客户端”代替“调用方”,以此与委派生成器(也是调用方,由于它调用了子生成器)区分开。
下图能更好地说明 yield from 结构的用法。图中把该示例中各个相关的部分标识出来了
委派生成器在 yield from 表达式处暂停时,调用方能够直接把数据发给子生成器,子生成器再把产出的值发给调用方。子生成器返回以后,解释器会抛出 StopIteration 异常,并把返回值附加到异常对象上,此时委派生成器会恢复
coroaverager3.py 脚本从一个字典中读取虚构的七年级男女学生的体重和身高。例如, 'boys;m' 键对应于 9 个男学生的身高(单位是米),'girls;kg' 键对应于 10 个女学生的体重(单位是千克)。这个脚本把各组数据传给前面定义的 averager 协程,而后生成一个报告,以下所示:
$ python3 coroaverager3.py 9 boys averaging 40.42kg 9 boys averaging 1.39m 10 girls averaging 42.04kg 10 girls averaging 1.43m
🌰 coroaverager3.py:使用 yield from 计算平均值并输出统计报告
1 from collections import namedtuple 2 3 4 Result = namedtuple('Result', 'count average') 5 6 7 # 子生成器 8 def averager(): # 子生成器 9 total = 0.0 10 count = 0 11 average = None 12 while True: 13 term = yield # 经过main函数中的gourp.send()接收到term的值 14 if term is None: # 相当重要的终止条件,告诉协程全部的数据已经结束,结束协程 15 break 16 total += term 17 count += 1 18 average = total/count 19 return Result(count, average) # 返回 grouper 中yield from的值 20 21 22 # 委派生成器 23 def grouper(results, key): # 委派生成器 24 while True: # 每次循环都会建立一个averager的实例 25 results[key] = yield from averager() # grouper发送的每一个值都会让yield from处理,把产出的值绑定给resuluts[key] 26 27 28 # 客户端代码,即调用方 29 def main(data): # main函数是客户端代码 30 results = {} 31 for key, values in data.items(): 32 group = grouper(results, key) # group是调用grouper的生成器 33 next(group) # 预激group协程 34 for value in values: 35 group.send(value) # 把各个value的值传递给grouper,经过grouper传入averager中term 36 group.send(None) # 全部值传递结束之后,终止averager 37 #print(results) # 若是要调试,去掉注释 38 report(results) 39 40 #输出报告 41 def report(results): 42 for key, result in sorted(results.items()): 43 group, unit = key.split(';') 44 print('{:2} {:5} averaging {:.2f}{}'.format( 45 result.count, group, result.average, unit)) 46 47 48 data = { 49 'girls;kg': 50 [40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5], 51 'girls;m': 52 [1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43], 53 'boys;kg': 54 [39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3], 55 'boys;m': 56 [1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46], 57 } 58 59 if __name__ == '__main__': 60 main(data)
以上代码执行的结果为:
9 boys averaging 40.42kg 9 boys averaging 1.39m 10 girls averaging 42.04kg 10 girls averaging 1.43m
下面简要说明上面🌰的运做方式,还会说明把 main 函数中调用group.send(None) 那一行代码(带有“重要!”注释的那一行)去掉会发生什么事。
yield from的意义
把迭代器看成生成器使用,至关于把子生成器的定义体内联在yield from 表达式中。此外,子生成器能够执行 return 语句,返回一个值,而返回的值会成为 yield from 表达式的值
批准后的 PEP 380 在“Proposal”一节(https://www.python.org/dev/peps/pep-0380/#proposal)分六点说明了yield from 的行为。这里,我几乎原封不动地引述,不过把有歧义的“迭代器”一词都换成了“子生成器”,还作了进一步说明。示例阐明了下述四点。
yield from 结构的另外两个特性与异常和终止有关
使用案例:使用协程作离散事件仿真
协程能天然地表述不少算法,例如仿真、游戏、异步 I/O,以及其余事件驱动型编程形式或协做式多任务。
离散事件仿真简介
离散事件仿真(Discrete Event Simulation,DES)是一种把系统建模成一系列事件的仿真类型。在离散事件仿真中,仿真“钟”向前推动的量不是固定的,而是直接推动到下一个事件模型的模拟时间。假如咱们抽象模拟出租车的运营过程,其中一个事件是乘客上车,下一个事件则是乘客下车。无论乘客坐了 5 分钟仍是 50 分钟,一旦乘客下车,仿真钟就会更新,指向这次运营的结束时间。使用离散事件仿真能够在不到一秒钟的时间内模拟一年的出租车运营过程。这与连续仿真不一样,连续仿真的仿真钟以固定的量(一般很小)不断向前推动。
显然,回合制游戏就是离散事件仿真的例子:游戏的状态只在玩家操做时变化,并且一旦玩家决定下一步怎么走了,仿真钟就会冻结。而实时游戏则是连续仿真,仿真钟一直在运行,游戏的状态在一秒钟以内更新不少次,所以反应慢的玩家特别吃亏。
这两种仿真类型都能使用多线程或在单个线程中使用面向事件的编程技术(例如事件循环驱动的回调或协程)实现。能够说,为了实现连续仿真,在多个线程中处理实时并行的操做更天然。而协程刚好为实现离散事件仿真提供了合理的抽象。SimPy 是一个实现离散事件仿真的Python 包,经过一个协程表示离散事件仿真系统中的各个进程。