Python中的协程大概经历了如下三个阶段:
最初的生成器变形yield/send
引入@asyncio.coroutine和yield from
在最近的Python3.5版本中引入async/await关键字
一:生成器变形yield/send
普通函数中如果出现了yield关键字,那么该函数就不再是普通函数,而是一个生成器。
1 | In [1]: def mygen(alist): |
像上面代码中的c就是一个生成器。生成器就是一种迭代器,可以使用for
进行迭代。生成器函数最大的特点是可以接受外部传入的一个变量,并根据变量内容计算结果后返回。这一切都是靠生成器内部的send()
函数实现的。
1 | In [5]: def gen(): |
上面生成器函数中最关键也是最易理解错的,就是receive=yield value
这句,如果对循环体的执行步骤理解错误,就会失之毫厘,差之千里。
其实receive=yield value
包含了3个步骤:
- 向函数外抛出(返回)value
- 暂停(pause),等待next()或send()恢复
- 赋值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 | In [11]: def g1(): |
这说明yield
就是将range
这个可迭代对象直接返回了。
而yield from
解析了range
对象,将其中每一个item
返回了。yield from iterable
本质上等于for item in iterable: yield item
的缩写版
来看一下例子,假设我们已经编写好一个斐波那契数列函数
1 | In [19]: def fab(max): |
fab
不是一个普通函数,而是一个生成器。因此fab(5)
并没有执行函数,而是返回一个生成器对象(生成器一定是迭代器iterator
,迭代器一定是可迭代对象iterable
)
现在我们来看一下,假设要在fab()
的基础上实现一个函数,调用起始都要记录日志
1 | In [23]: def f_wrapper(fun_iterable): |
现在用yield from
代替for
循环
1 | In [29]: def f_wrapper2(fun_iterable): |
再强调一遍:yield from后面必须跟iterable对象(可以是生成器,迭代器)
三:asyncio.coroutine和yield from
yield from
在asyncio模块中得以发扬光大。之前都是我们手工切换协程,现在当声明函数为协程后,我们通过事件循环来调度协程。
实例代码:
1 | In [32]: import asyncio,random |
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 | yield from time.sleep(sleep_secs) |
四:async和await
弄清楚了asyncio.coroutine
和yield from
之后,在Python3.5中引入的async和await就不难理解了:可以将他们理解成asyncio.coroutine/yield from
的完美替身。当然,从Python设计的角度来说,async/await
让协程表面上独立于生成器而存在,将细节都隐藏于asyncio模块之下,语法更清晰明了。
加入新的关键字 async ,可以将任何一个普通函数变成协程
1 | In [39]: import asyncio |
在上面程序中,我们在前面加上async,该函数就变成一个协程了。
但是async对生成器是无效的。async无法将一个生成器转换成协程。
还是刚才那段代码,我们把print
改成yield
1 | In [44]: async def mygen(alist): |
并不是coroutine 协程对象
所以我们的协程代码应该是这样的
1 | In [1]: import time,asyncio,random |
参考文章: