Python coroutine-depth understanding of (turn)

Original: https://www.cnblogs.com/zhaof/p/7631851.html

Grammatically, generators and the like coroutine, yield keyword is a function included in the definition body.
In coroutine yield Usage:

  • In coroutine yield typically occurs in the expression on the right, for example: datum = yield, can produce a value, the output may not be - if there is no back expression yield keyword, then the generator output None.
  • Coroutine likely to accept data from the caller, the caller by send (datum) data to the way the coroutine used instead Next (...) function, the value will generally call party pushed to the coroutine.
  • Coroutine can give centralized scheduler controller to activate another coroutine

Therefore, generally the yield in the coroutine seen as a flow of control mode.

Understand the process of coroutines

First through a simple example coroutine understanding:

Analysis of the above example:
yield is not the right expression, so here is default value output None
beginning to call the next (...) because this time the generator has not started, the yield did not stop there, this time it was unable to send data via send. So when we (...) after activation coroutine, the program will run through the next x = yield, there is a problem here that we need to pay attention, x = yield of this expression calculation process is to calculate the content of the right of the equal sign, then making an assignment, so when the generator is activated, the program will stop at the yield here, but did not give x assignment.
When we call the send method will receive this value and yield assigned to x, and when the program runs to the end of the body and Association Process Definition when the generator with the same will raise StopIteration

If the coroutine does not pass next (...) activate (Similarly, we can activate by send (None) the way), but we directly send, it will prompt the following error:

About call next (...) function This step is often referred to as "pre-excitation (prime)" coroutine that let coroutine forward to the implementation of the first expression yield, ready for use as an active coroutine

协程在运行过程中有四个状态:

  1. GEN_CREATE:等待开始执行
  2. GEN_RUNNING:解释器正在执行,这个状态一般看不到
  3. GEN_SUSPENDED:在yield表达式处暂停
  4. GEN_CLOSED:执行结束

通过下面例子来查看协程的状态:

接着再通过一个计算平均值的例子来继续理解:

这里是一个死循环,只要不停send值给协程,可以一直计算下去。
通过上面的几个例子我们发现,我们如果想要开始使用协程的时候必须通过next(...)方式激活协程,如果不预激,这个协程就无法使用,如果哪天在代码中遗忘了那么就出问题了,所以有一种预激协程的装饰器,可以帮助我们干这件事

预激协程的装饰器

下面是预激装饰器的演示例子:

复制代码
 1 from functools import wraps
 2 
 3 
 4 def coroutine(func):
 5     @wraps(func)
 6     def primer(*args,**kwargs):
 7         gen = func(*args,**kwargs)
 8         next(gen)
 9         return gen
10     return primer
11 
12 
13 @coroutine
14 def averager():
15     total = 0.0
16     count = 0
17     average = None
18     while True:
19         term = yield average
20         total += term
21         count += 1
22         average = total/count
23 
24 
25 coro_avg = averager()
26 from inspect import getgeneratorstate
27 print(getgeneratorstate(coro_avg))
28 print(coro_avg.send(10))
29 print(coro_avg.send(30))
30 print(coro_avg.send(5))
复制代码

关于预激,在使用yield from句法调用协程的时候,会自动预激活,这样其实与我们上面定义的coroutine装饰器是不兼容的,在python3.4里面的asyncio.coroutine装饰器不会预激协程,因此兼容yield from

终止协程和异常处理

协程中为处理的异常会向上冒泡,传给next函数或send函数的调用方(即触发协程的对象)
拿上面的代码举例子,如果我们发送了一个字符串而不是一个整数的时候就会报错,并且这个时候协程是被终止了

从python2.5开始客户端代码在生成器对象上调用两个方法,显示的把异常发送给协程
分别为:throw和close
generator.throw:会让生成器在暂停的yield表达式处抛出指定的异常,如果生成器处理了抛出的异常,代码会向前执行到下一个yield表达式,而产出的值会成为调用generator.throw方法代码的返回值。如果生成器没有处理抛出的异常,异常会向上冒泡,传到调用方的上下文中。
generator.close:会让生成器在暂停的yield表达式处抛出GeneratorExit异常。如果生成器没有处理这个异常,或者抛出了StopIteration异常,调用方不会报错,如果收到GeneratorExit异常,生成器一定不能产出值,否则解释器会抛出RuntimeError异常。生成器抛出的异常会向上冒泡,传给调用方。
下面是一个例子:

当传入我们定义的异常时不会影响协程,协程不会停止,可以继续send,但是如果是没有处理的异常的时候,就会报错,并且协程会被终止

让协程返回值

通过下面的例子进行演示如何获取协程的返回值:

复制代码
 1 from collections import namedtuple
 2 
 3 
 4 Result = namedtuple("Result","colunt average")
 5 
 6 
 7 def averager():
 8     total = 0.0
 9     count = 0
10     average = None
11     while True:
12         term = yield
13         if term is None:
14             break
15         total += term
16         count+=1
17         average = total/count
18     return Result(count,average)
19 
20 coro_avg = averager()
21 next(coro_avg)
22 coro_avg.send(10)
23 coro_avg.send(30)
24 coro_avg.send(5)
25 try:
26     coro_avg.send(None)
27 except StopIteration as e:
28     result = e.value
29     print(result)
复制代码

这样就可以获取到最后的结果:

其实相对来说上面这种方式获取返回值比较麻烦,而yield from 结构会自动捕获StopIteration异常,这种处理方式与for循环处理StopIteration异常的方式一样,循环机制使我们更容易理解处理异常,对于yield from来说,解释器不仅会捕获StopIteration异常,还会把value属性的值变成yield from表达式的值

关于yield from

在生成器gen中使用yield from subgen()时,subgen会获得控制权,把产出的值传给gen的调用方,即调用方可以直接控制subgen,同时,gen会阻塞,等待subgen终止

yield from x表达式对x对象所做的第一件事是,调用iter(x),从中获取迭代器,因此x可以是任何可迭代的对象

下面是yield from可以简化yield表达式的例子:

 

复制代码
 1 def gen():
 2     for c in "AB":
 3         yield c
 4     for i in range(1,3):
 5         yield i
 6 
 7 print(list(gen()))
 8 
 9 def gen2():
10     yield from "AB"
11     yield from range(1,3)
12 
13 print(list(gen2()))
复制代码

这两种的方式的结果是一样的,但是这样看来yield from更加简洁,但是yield from的作用可不仅仅是替代产出值的嵌套for循环。
yield from的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接发送和产出值,还可以直接传入异常,而不用再像之前那样在位于中间的协程中添加大量处理异常的代码

通过yield from还可以链接可迭代对象

委派生成器在yield from 表达式处暂停时,调用方可以直接把数据发给子生成器,子生成器再把产出产出值发给调用方,子生成器返回之后,解释器会抛出StopIteration异常,并把返回值附加到异常对象上,此时委派生成器会恢复。

下面是一个完整的例子代码

复制代码
 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
14         if term is None:
15             break
16         total += term
17         count += 1
18         average = total/count
19     return Result(count, average)
20 
21 
22 # 委派生成器
23 def grouper(result, key):
24     while True:
25         result[key] = yield from averager()
26 
27 
28 # 客户端代码,即调用方
29 def main(data):
30     results = {}
31     for key,values in data.items():
32         group = grouper(results,key)
33         next(group)
34         for value in values:
35             group.send(value)
36         group.send(None) #这里表示要终止了
37 
38     report(results)
39 
40 
41 # 输出报告
42 def report(results):
43     for key, result in sorted(results.items()):
44         group, unit = key.split(';')
45         print('{:2} {:5} averaging {:.2f}{}'.format(
46             result.count, group, result.average, unit
47         ))
48 
49 data = {
50     'girls;kg':
51         [40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5],
52     'girls;m':
53         [1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43],
54     'boys;kg':
55         [39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3],
56     'boys;m':
57         [1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46],
58 }
59 
60 
61 if __name__ == '__main__':
62     main(data)
复制代码

关于上述代码着重解释一下关于委派生成器部分,这里的循环每次迭代时会新建一个averager实例,每个实例都是作为协程使用的生成器对象。

grouper发送的每个值都会经由yield from处理,通过管道传给averager实例。grouper会在yield from表达式处暂停,等待averager实例处理客户端发来的值。averager实例运行完毕后,返回的值会绑定到results[key]上,while 循环会不断创建averager实例,处理更多的值

并且上述代码中的子生成器可以使用return 返回一个值,而返回的值会成为yield from表达式的值。

关于yield from的意义

关于yield from 六点重要的说明:

  1. 子生成器产出的值都直接传给委派生成器的调用方(即客户端代码)
  2. 使用send()方法发送给委派生成器的值都直接传给子生成器。如果发送的值为None,那么会给委派调用子生成器的__next__()方法。如果发送的值不是None,那么会调用子生成器的send方法,如果调用的方法抛出StopIteration异常,那么委派生成器恢复运行,任何其他异常都会向上冒泡,传给委派生成器
  3. 生成器退出时,生成器(或子生成器)中的return expr表达式会出发StopIteration(expr)异常抛出
  4. yield from表达式的值是子生成器终止时传给StopIteration异常的第一个参数。yield from 结构的另外两个特性与异常和终止有关。
  5. 传入委派生成器的异常,除了GeneratorExit之外都传给子生成器的throw()方法。如果调用throw()方法时抛出StopIteration异常,委派生成器恢复运行。StopIteration之外的异常会向上冒泡,传给委派生成器
  6. 如果把GeneratorExit异常传入委派生成器,或者在委派生成器上调用close()方法,那么在子生成器上调用clsoe()方法,如果它有的话。如果调用close()方法导致异常抛出,那么异常会向上冒泡,传给委派生成器,否则委派生成器抛出GeneratorExit异常

 

Guess you like

Origin www.cnblogs.com/ajianbeyourself/p/11261302.html