协程

不问归期 提交于 2021-02-17 01:58:16

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

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!