之前都提到了几次协程,那么今天就来具体看看到底什么是协程,为什么要有协程,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的具体语义很难理解,尤其是处理异常的那两点,这里先点到为止,等以后有时间再详细研究下。
来源:oschina
链接:https://my.oschina.net/u/4267186/blog/3620893