协程
之前都提到了几次协程,那么今天就来具体看看到底什么是协程,为什么要有协程,python如何实现协程的。
通过对生成器的学习,我们知道,yield val这行代码会给调用next(gen)的客户端产出一个值,然后暂停,把执行权限移交到客户端那里,等到下次客户端再次调用next(gen)的时候,再从yield后面的代码处接着执行,感觉是不是很像两个人在协作着完成一件事情?
没错,协程就是这样的,通过多个组件之间的互相协作,来完成某件事情。在python中,从语法上看,协程跟生成器类似,都是定义体中包含yield关键字的函数。但是,在协程中,yield通常出现在表达式的右边(如 ret = yield val),可以产出值,也可以不产出,如果yield后面没有表达式,那么生成器产出None,协程通常会从调用方接收数据,调用方把数据提供给协程的方法就是.send(data)方法。通常而言,调用方会把值推送给协程。
yield关键字甚至还可以不接收或者传出数据。不管数据如何流动,yield都可以看成是一种控制流程的工具,利用它可以实现协作式任务,这就是协程的工作流程模型。
生成器如何变成协程的
协程指的是一个过程,这个过程与调用方协作,产出由调用方提供的值。
除了.send()方法外,还有.thow(),.close()方法。
.send()
和next()类似,只是.send()方法,必须在生成器已经返回一次值(next)之后才可以使用
.throw()
调用方抛出异常,在生成器中处理
.close()
终止生成器
用作协程的生成器基本行为
先来看一个最简单的协程示例:
1 def simple_coroutine(): 2 print('-> coroutine start') 3 x = yield 4 print('-> coroutine received:', x) 5 6 corou = simple_coroutine() 7 next(corou) #必须先调用,到yield,返回None时暂停,接下来才能执行corou.send(6) 8 9 corou.send(6) # x = yield , 6被绑定到x上
代码最后因为没有值产出了,所以抛出StopIteration异常(迭代器协议)。
协程有四种状态,可以使用inspect.getgeneratorstate(...)函数来确定,该函数返回下述字符串中的其一:
'GEN_CREATED'
等待开始执行
'GEN_RUNNING'
正在执行
'GEN_SUSPENDED'
在yield出暂停
'GEN_CLOSED'
执行结束
send方法的参数会成为暂停的yield表达式的值,仅当协程处于GEN_SUSPENDED状态时,才可以调用send方法。不过如果协程还没有被激活(GEN_CREATED),可以通过next(corou)激活,使用corou.send(None)也是一样的。如果发送None以外的值会发生错误。
corou = simple_coroutine()
corou.send(6)
-----------------------------------------------------------------------------------------------------
File "E:\test.py", line 1353, in <module>
corou.send(6)
TypeError: can't send non-None value to a just-started generator
错误提示也很明显,不能发送除None以外的值给未激活的生成器。
事先调用next(corou)函数这一步通常被称为“预激”(prime)协程(让协程执行到第一个yield表达式语句处,准备作为激活协程使用)。
再来看一个例子:
1 def simple_coro2(a): 2 print('-> Start: a =', a) 3 b = yield a 4 print('-> Recvied: b =', b) 5 c = yield a+b 6 print('-> received: c =', c) 7 8 coro = simple_coro2(3) 9 next(coro) 10 #a=3, yield 3 11 coro.send(10) 12 #b=10 yield 3+10 13 coro.send(1) 14 #c=1, StopIteration
牢记一点:协程会在yield关键字的位置处返回产出的值,并暂停,知道调用方调用next或者send方法后,再接着yield后面的语句执行,如果yield语句在赋值表达式的右边,那么赋值操作也包括在内。
使用协程计算移动平均值
1 def average(): 2 s, n = 0, 0 3 val = yield 4 while True: 5 s += val 6 n += 1 7 val = yield s/n 8 9 aver = average() 10 next(aver) 11 print('Start') 12 print(aver.send(1)) 13 print(aver.send(2)) 14 print(aver.send(3)) 15 print(aver.send(4)) 16 print(aver.send(5)) 17 print(aver.send(6)) 18 print(aver.send(7))
while循环外面,协程激活(next)时,yield返回None,客户端调用send方法发送数据时由val变量接收,然后是个死循环,依次产出当前的平均值,同时val保存客户端send发送的新值。
从上面的例子中我们可以知道,要使用协程必须先预激活,然而这一步很容易忘记。接下来我们将使用一个特殊的装饰器帮助我们完成这一步。
预激协程的装饰器
如果不预激,那么协程没什么用,一定要先调用next(gen),为了简化协程的用法,有时会使用一个预激装饰器,functools.wraps会替换原来的函。
示例如下:
1 import functools 2 3 def coroutine(func): 4 """ 5 use functools.wraps to auto activate coroutine 6 """ 7 @functools.wraps(func) 8 def task(*args, **kwargs): 9 gen = func(*args, **kwargs) 10 next(gen) 11 return gen 12 return task 13 14 @coroutine 15 def average(): 16 s, n = 0, 0 17 val = yield 18 while True: 19 s += val 20 n += 1 21 val = yield s/n 22 23 aver = average() 24 25 print(aver.send(1)) 26 print(aver.send(2)) 27 print(aver.send(3)) 28 print(aver.send(4)) 29 print(aver.send(5)) 30 print(aver.send(6))
本质上就是定义一个装饰器,用我们的函数去自动完成预激的功能,把它运用在具体生成器上,替换原来的函数。不过不是所有的装饰器都用于预激协程,有些还会提供其他服务,例如勾入事件循环。比如,异步网络库Tornado提供了tornado.gen装饰器。
使用yield from语法调用协程时,会自动预激,因此与@cotoutine不兼容。而@asyncio.coroutine装饰器不会预激协程,因此兼容yield from句法。
终止协程和异常处理
协程中未处理的异常都会向上冒泡,传给next和send的调用方。
1 print(aver.send(40)) 2 print(aver.send('spam')) 3 """ 4 TypeError: unsupported operand type(s) for +=: 'int' and 'str' 5 """
原因很明显,'spam'不能和数字相加。这里暗示了终止协程的方式:发送某个哨符,让协程退出。内置的None和Ellipsis京彩充当这个值。Ellipsis的优点是数据流中不太常有这个值。甚至还有人把StopItreration作为哨符,即aver.send(StopIteration)。
从python2.5开始,客户端可以调用两个方法显示把异常发给协程,throw和close方法。
generator.throw(exc_type[, exc_value[, traceback])
使生气在暂停的yield表达式出抛出指定的异常,如果生成器处理了指定的异常,代码会向前执行到下一个yield表达式,而产出的值会变成调用generator.throw的返回值。如果生成器没有处理抛出的异常,异常会向上冒泡,传到调用的客户端代码中。
generator.close()
使生成器在暂停的yield表达式抛出GeneratorExit异常。如果生成器没有处理这个异常,或者抛出了StopIteration异常,调用方不会报错,如果收到GeneratorExit异常,生成器一定不能产出值,否则解释器会抛出RuntimeError异常。生成器抛出的其他的异常会向上冒泡,传给调用方。
让协程返回值
为了说明如何返回值,我们不计算移动平均值,而是让返回的结果是一个namedtuple,他有两项分别是项数(count)和平均值(average)。
1 from collections import namedtuple 2 3 Result = namedtuple('Result', 'count average') 4 5 def averager(): 6 total, count = 0.0, 0 7 average = None 8 while True: 9 term = yield 10 if term is None: 11 break 12 total += term 13 count += 1 14 average = total/count 15 return Result(count, average) 16 17 18 aver = averager() 19 next(aver) 20 aver.send(100) 21 print(aver.send(None))
注意:python3.3之前如果生成器返回值,会报语法错误。
发送None会导致终止循环,导致协程结束,返回结果。生成器对象抛出StopIteration异常,异常对象的value属性保存着返回的值。
注意:result表达式的值会偷偷传给调用方,赋值给StopIteration的value属性。尽管这样做有点不合常理,但是能保留生成器对象的常规行为----耗尽时抛出StopIteration异常。
如果要捕获返回值:
1 try: 2 print(aver.send(None)) 3 except StopIteration as e: 4 print(e.value) 5 6 """ 7 Result(count=1, average=100.0) 8 """
获取协程的返回值虽然要绕个弯,但这是PEP380定义的方式,这样就说的通了:yield from结构会在内部自动捕获StopIteration异常。这种处理跟for循环处理StopIteration异常的方式一样:循环机制以用户友好的方式处理异常值。对于yield from结构来说,解释器不仅会捕获StopIteration异常,还会把value属性变为yield from表达式的值。
使用yield from
yield from是新的语言结构。它的作用比yield要多很多,因此人们认为继续使用那个关键字多少会引起误解。在生成器gen中使用 yield from subgen(),subgen会获得控制权,把产出的值传给gen的调用方,即调用方可以直接控制subgen。同时,gen会阻塞,等待subgen终止。
yield from可以简化for循环中的yield:
1 """ 2 def gen(): 3 for c in 'AB': 4 yield c 5 6 for i in range(1, 3): 7 yield i 8 """ 9 10 def gen(): 11 yield from 'AB' 12 yield from range(1, 3) 13 for i in gen(): 14 print(i)
yield from还可以链接可迭代对象:
1 def chain(*iterables): 2 for it in iterables: 3 yield from it 4 5 s = 'ABC' 6 t = tuple(range(3)) 7 print(list(chain(s, t)))
yield from x这种表达式所做的第一件事就是调用iter(x),获取x的迭代器。因此x必须要是可迭代对象。
yield from的主要功能是打开双向通道,把最外层的调用方与最内侧的子生成器连接起来,这样二者就可以直接发送和产出值,还可以传入异常,从而避免在中间的协程中添加大量处理异常的样板代码。有了这种结构,协程可以通过不可能的方式委托职责。
先来看一下yield from结构里用到的一些专门术语。
委派生成器
包含yield from <iterable>表达式的生成器函数
子生成器
从yield from表达式中<iterable>部分获取的生成器
调用方
调用委派生成器的客户端代码
下面这张图能更好的说明三者的关系。
委派生成器在yield from表达式处暂停,调用方可以直接把数据发给子生成器,子生成器再把产出的值发给调用方。子生成器返回之后,解释器抛出StopIteration异常,把返回值附加到一场对象上,此时委派生成器会恢复。
假设我们要从字典中读取学生的体重和身高。例如'boys;m'键对应男学生的身高,'girls;kg'对应女学生的体重。我们希望生成一个报告如下:
9 boys averaging 40.42kg
9 boys averaging 1.39m
10 girls averaging 42.04kg
10 girls averaging 1.43m
1 from collections import namedtuple 2 Result = namedtuple('Result', 'count average') 3 4 #子生成器 5 def averager(): 6 total, count = 0.0, 0 7 average = None 8 while True: 9 term = yield 10 if term is None: 11 break 12 total += term 13 count += 1 14 average = total/count 15 return Result(count, average) 16 17 #委派生成器 18 def grouper(results, key): 19 while True: #这里的循环时必须的,如果没有循环,那么结束子生成器时会抛出StopIteration异常 20 results[key] = yield from averager() #StopIteration中value属性的值会被赋给yield from表达式的值 21 22 #客户端代码 23 def main(data): 24 results = {} 25 for key, vals in data.items(): 26 group = grouper(results, key) 27 next(group) #预激协程 28 for val in vals: 29 group.send(val) 30 group.send(None) #一定要结束协程,不然协程还在死循环,永远得不到Result(count, average),而是None 31 print(results) 32 report(results) 33 34 def report(results): 35 for key, result in sorted(results.items()): 36 group, unit = key.split(';') 37 print('{:2} {:5} averaging {:.2f}{}'.format 38 (result.count, group, result.average, unit)) 39 40 main(data)
这里averager是子生成器,当客户端send(None)是退出,把Result(count, average)返回,grouper是我们新增加的委派生成器,每次迭代时新建一个averager实例,作为协程使用的生成器对象,grouper发送的每个值都会由yield from处理,通过管道传给averager实例,grouper会在yield from表达式处暂停,等待averager实例返回并抛出StopIteration异常,捕获异常把异常值(averager实例的返回值)赋给results[key],while循环不断创建averager实例,处理更多值。在传值的过程中,值是发送给averager实例的,grouper永远不知道传入的是什么值。当传入的值是None时,averager实例终止,grouper获得返回值,然后继续下一组循环,继续创建averager实例,处理下一组值。
这段代码的运行方式如下:
* 外层for循环每次迭代会创建一个grouper实例,绑定到变量group;group即是委派生成器
* 调用next(group),预激委派生成器,此时进入while True循环,调用子生成器averager,在yield from表达式处暂停
* 内层for循环调用group.send(value),直接把值传给子生成器averager实例,同时在当前的grouper实例在yield from表达式处暂停
* 内层循环结束后,group实例依旧在yield from表达式处暂停,因为grouper函数定义体中results[key]赋值的语句还没有执行
* 如果外层for循环的末尾没有group.send(None),那么averager子生成器永远不会终止,委派生成器group也永远不会再次激活,因此results永远是空字典
* 外层for循环重新迭代时新创建一个grouper实例,上一个被垃圾回收
以上时yield from结构最简单的用法,只有一个委派生成器和一个子生成器。
因为委派生成器相当于管道,所以可以把任意数量的委派生成器连接在一起:一个委派生成器使用yield from调用一个子生成器,而那个子生成器本身也是委派生成器,使用yield from调用另一个子生成器,一次类推。最终,这种链式调用以yield表达式的简单生成器结束。
任何的yield from链条都必须有客户驱动,在最外层委派生成器调用next函数或发送send发送。
yield from的意义
PEP380在“Proposal”一节中分六点说明了yield from的行为:
* 子生成器产出的值都直接传给委派生成器的调用方
* 使用send()方法发给委派生成器的值都会直接传给子生成器。如果发送的值是None,那么会调用子生成器的__next__()方法,如果不是None,那么调用子生成器的send()方法。如果调用的方法抛出StopIteration异常,那么委派生成器恢复运行,任何其他异常都会向上冒泡,传给委派生成器
* 生成器退出时,生成器或子生成器中的return expr会出发StopIteration异常抛出
* yield from表达式的值是子生成器终止时传给StopIteration异常的第一个参数
yield from结构的另外两个特性和异常和终止有关
* 传入委派生成器的异常,除了GeneratorExit之外都传给子生成器的throw()方法,如果throw方法抛出StopIteration异常,委派生成器恢复运行,StopIteration之外的异常向上冒泡,传给委派生成器
* 如果把GeneratorExit异常传入委派生成器,或者在委派生成器上调用close()方法,那么在子生成器上调用close()方法,如果调用close()方法导致异常,异常向上冒泡,传给委派生成器,否则,委派生成器抛出GeneratorExit异常。
关于yield from的具体语义很难理解,尤其是处理异常的那两点,这里先点到为止,等以后有时间再详细研究下。
posted on 2019-03-11 14:54 forwardFields 阅读(449) 评论(0) 编辑 收藏 举报