理解Python协程:从yield/send到yield-from再到async/await

Python中的协程大概经历了如下三个阶段:

  1. 最初的生成器变形yield/send

  2. 引入@asyncio.coroutine和yield from

  3. 在最近的Python3.5版本中引入async/await关键字

一:生成器变形yield/send

普通函数中如果出现了yield关键字,那么该函数就不再是普通函数,而是一个生成器。

1
2
3
4
5
6
7
8
9
10
11
12
In [1]: def mygen(alist):
...: while len(alist) > 0:
...: c = randint(0, len(alist)-1)
...: yield alist.pop(c)
...:

In [2]: a = ["aa","bb","cc"]

In [3]: c=mygen(a)

In [4]: print(c)
<generator object mygen at 0x106119360>

像上面代码中的c就是一个生成器。生成器就是一种迭代器,可以使用for进行迭代。生成器函数最大的特点是可以接受外部传入的一个变量,并根据变量内容计算结果后返回。这一切都是靠生成器内部的send()函数实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
In [5]: def gen():
...: value=0
...: while True:
...: receive = yield value
...: if receive=='e':
...: break
...: value = f"get: {receive}"
...:

In [6]: g=gen()

In [7]: g.send(None)
Out[7]: 0

In [8]: g.send('hello')
Out[8]: 'get: hello'

In [9]: g.send(123456)
Out[9]: 'get: 123456'

In [10]: g.send('e')
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-10-422bc441ae72> in <module>()
----> 1 g.send('e')

StopIteration:

上面生成器函数中最关键也是最易理解错的,就是receive=yield value这句,如果对循环体的执行步骤理解错误,就会失之毫厘,差之千里。
其实receive=yield value包含了3个步骤:

  1. 向函数外抛出(返回)value
  2. 暂停(pause),等待next()或send()恢复
  3. 赋值receive=MockGetValue() 。 这个MockGetValue()是假想函数,用来接收send()发送进来的值

执行流程:

1、通过g.send(None)或者next(g)启动生成器函数,并执行到第一个yield语句结束的位置。

这里是关键,很多人就是在这里搞糊涂的。运行receive=yield value语句时,我们按照开始说的拆开来看,实际程序只执行了1,2两步,程序返回了value值,并暂停(pause),并没有执行第3步给receive赋值。因此yield value会输出初始值0。

这里要特别注意:在启动生成器函数时只能send(None),如果试图输入其它的值都会得到错误提示信息。

2、通过g.send('hello'),会传入hello,从上次暂停的位置继续执行,那么就是运行第3步,赋值给receive。然后计算出value的值,并回到while头部,遇到yield value,程序再次执行了1,2两步,程序返回了value值,并暂停(pause)。此时yield value会输出”get: hello”,并等待send()激活。

3、通过g.send(123456),会重复第2步,最后输出结果为”get: 123456″

4、当我们g.send(‘e’)时,程序会执行break然后推出循环,最后整个函数执行完毕,所以会得到StopIteration异常。

从上面可以看出, 在第一次send(None)启动生成器(执行1–>2,通常第一次返回的值没有什么用)之后,对于外部的每一次send(),生成器的实际在循环中的运行顺序是3–>1–>2,也就是先获取值,然后dosomething,然后返回一个值,再暂停等待。

二:yield from

看一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
In [11]: def g1():
...: yield range(5)
...:

In [12]: def g2():
...: yield from range(5)
...: it1 = g1()
...:

In [13]: it2 = g2()

In [15]: it1 = g1()

In [16]: for x in it1:
...: print(x)
...:
range(0, 5)

In [17]: for x in it2:
...: print(x)
...:
...:
0
1
2
3
4

这说明yield就是将range这个可迭代对象直接返回了。
yield from解析了range对象,将其中每一个item返回了。
yield from iterable本质上等于for item in iterable: yield item的缩写版

来看一下例子,假设我们已经编写好一个斐波那契数列函数

1
2
3
4
5
6
7
8
9
10
11
12
13
In [19]: def fab(max):
...: n,a,b = 0,0,1
...: while n < max:
...: yield b
...: # print b
...: a, b = b, a + b
...: n = n + 1
...:

In [20]: f = fab(5)

In [21]: f
Out[21]: <generator object fab at 0x106119728>

fab不是一个普通函数,而是一个生成器。因此fab(5)并没有执行函数,而是返回一个生成器对象(生成器一定是迭代器iterator,迭代器一定是可迭代对象iterable)
现在我们来看一下,假设要在fab()的基础上实现一个函数,调用起始都要记录日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
In [23]: def f_wrapper(fun_iterable):
...: print('start')
...: for item in fun_iterable:
...: yield item
...: print('end')
...:

In [27]: wrap = f_wrapper(fab(5))

In [28]: for i in wrap:
...: print(i)
...:
start
1
1
2
3
5
end

现在用yield from代替for循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
In [29]: def f_wrapper2(fun_iterable):
...: print('start')
...: yield from fun_iterable # 注意此处必须是一个可迭代对象
...: print('end')
...:

In [30]: wrap = f_wrapper2(fab(5))

In [31]: for i in wrap:
...: print(i)
...:
start
1
1
2
3
5
end

再强调一遍:yield from后面必须跟iterable对象(可以是生成器,迭代器)

三:asyncio.coroutine和yield from

yield from在asyncio模块中得以发扬光大。之前都是我们手工切换协程,现在当声明函数为协程后,我们通过事件循环来调度协程。

实例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
In [32]: import asyncio,random

In [33]: @asyncio.coroutine
...: def smart_fib(n):
...: index = 0
...: a = 0
...: b = 1
...: while index < n:
...: sleep_secs = random.uniform(0, 0.2)
...: yield from asyncio.sleep(sleep_secs) #通常yield from后都是接的耗时操作
...: print('Smart one think {} secs to get {}'.format(sleep_secs, b))
...: a, b = b, a + b
...: index += 1
...:

In [34]: @asyncio.coroutine
...: def stupid_fib(n):
...: index = 0
...: a = 0
...: b = 1
...: while index < n:
...: sleep_secs = random.uniform(0, 0.4)
...: yield from asyncio.sleep(sleep_secs) #通常yield from后都是接的耗时操作
...: print('Stupid one think {} secs to get {}'.format(sleep_secs, b))
...: a, b = b, a + b
...: index += 1
...:

In [35]: loop = asyncio.get_event_loop()

In [36]: tasks = [smart_fib(10), stupid_fib(10)]

In [37]: loop.run_until_complete(asyncio.wait(tasks))

In [38]: loop.close()

yield from语法可以让我们方便地调用另一个generator。
本例中yield from后面接的asyncio.sleep()是一个coroutine(里面也用了yield from),所以线程不会等待asyncio.sleep(),而是直接中断并执行下一个消息循环。当asyncio.sleep()返回时,线程就可以从yield from拿到返回值(此处是None),然后接着执行下一行语句。
asyncio是一个基于事件循环的实现异步I/O的模块。通过yield from,我们可以将协程asyncio.sleep的控制权交给事件循环,然后挂起当前协程;之后,由事件循环决定何时唤醒asyncio.sleep,接着向后执行代码。
协程之间的调度都是由事件循环决定。
yield from asyncio.sleep(sleep_secs) 这里不能用time.sleep(1)因为time.sleep()返回的是None,它不是iterable,还记得前面说的yield from后面必须跟iterable对象(可以是生成器,迭代器)。

所以会报错:

1
2
yield from time.sleep(sleep_secs) 
TypeError: ‘NoneType’ object is not iterable

四:async和await

弄清楚了asyncio.coroutineyield from之后,在Python3.5中引入的async和await就不难理解了:可以将他们理解成asyncio.coroutine/yield from的完美替身。当然,从Python设计的角度来说,async/await让协程表面上独立于生成器而存在,将细节都隐藏于asyncio模块之下,语法更清晰明了。

加入新的关键字 async ,可以将任何一个普通函数变成协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
In [39]: import asyncio

In [40]: async def mygen(alist):
...: while len(alist) > 0:
...: c = randint(0, len(alist)-1)
...: print(alist.pop(c))
...:

In [41]: a = ["aa","bb","cc"]

In [42]: c=mygen(a)

In [43]: c
Out[43]: <coroutine object mygen at 0x106d7fb48>

在上面程序中,我们在前面加上async,该函数就变成一个协程了。

但是async对生成器是无效的。async无法将一个生成器转换成协程。
还是刚才那段代码,我们把print改成yield

1
2
3
4
5
6
7
8
9
10
11
12
In [44]: async def mygen(alist):
...: while len(alist) > 0:
...: c = randint(0, len(alist)-1)
...: yield alist.pop(c)
...:

In [45]: a = ["aa","bb","cc"]

In [46]: c=mygen(a)

In [47]: c
Out[47]: <async_generator object mygen at 0x1067dfb40>

并不是coroutine 协程对象

所以我们的协程代码应该是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
In [1]: import time,asyncio,random

In [2]: async def mygen(alist):
...: while len(alist) > 0:
...: c = random.randint(0, len(alist)-1)
...: print(alist.pop(c))
...: await asyncio.sleep(1)
...:

In [3]: strlist = ["ss","dd","gg"]

In [4]: intlist=[1,2,5,6]

In [5]: c1=mygen(strlist)

In [6]: c2=mygen(intlist)

In [7]: loop = asyncio.get_event_loop()

In [8]: tasks = [c1,c2]

In [9]: loop.run_until_complete(asyncio.wait(tasks))
1
dd
5
ss
2
gg
6
Out[9]:
({<Task finished coro=<mygen() done, defined at <ipython-input-2-b81995509425>:1> result=None>,
<Task finished coro=<mygen() done, defined at <ipython-input-2-b81995509425>:1> result=None>},
set())

In [10]: loop.close()

参考文章:

理解Python协程:从yield/send到yield from再到async/await

深入理解Python中的生成器

Python Async/Await入门指南

Python 协程从零开始到放弃

知识就是财富
如果您觉得文章对您有帮助, 欢迎请我喝杯水!