面试被问到python3 的asyncio模块、python2中的yield和yield from的区别
面的第一家觉得回答的不是很好, 回来进行收集整理一番,以便巩固记忆
Python中的协程大概经历了三个阶段:
- 最初的生成器变形yield/send
- 引入@asyncio.coroutine和yield from
- 在最近的python3.5版本中引入async/await字段
下面将对上边的三个阶段进行说明
1、 生成器变形yield/send
普通函数中如果出现了yield, name该函数就不是普通的函数, 而是一个生成器
1 def gen(): 2 for i in range(1,10): 3 yield i 4 5 g = gen() 6 for i in g: 7 print(i)
像上面的代码 g 就是一个生成器, 生成器就是一种迭代器,可以使用for进行迭代,生成器最大的特点就是可以接受一个外部传入的变量,根据变脸内容计算返回结果
1 def gen(): 2 while True: 3 i = 0 4 x = yield i 5 if x == 0: 6 break 7 i = i*x 8 g = gen() 9 print(g.send(None)) 10 print(g.send(1)) 11 print(g.send(2)) 12 print(g.send(0))
上面的步骤中, x = yield i最重要也最容易理解错,下面详细接介绍该语句的运行流程
其实x = yield i包含了三个步骤:
- 向函数外抛出(返回) i
- 暂停,等待next()和send()恢复
- 赋值receive=MockGetValue() 。 这个MockGetValue()是假想函数,用来接收send()发送进来的值
执行流程:
- 通过g.send(None)或者next(g)启动生成器函数, 并执行到第一个yield语句停止,注意在这里相当于执行了上边的1,2步,程序返回yield后边的 i 值后暂停,并没有执行第三步, 注意第一次这里send(None)函数的参数必须传None否者会报错
- 通过g.send(1), 会传入值,从上一次暂停的位置继续执行,从第三步执行赋值操作后 再执行1,2步。
- 当我们传g.send(0)时候, 会主动break 并且退出循环,最后整个函数执行完毕,所以会得到StopIteration异常。
2、yield from
1 def g1(): 2 yield range(5) 3 def g2(): 4 yield from range(5) 5 6 it1 = g1() 7 it2 = g2() 8 for x in it1: 9 print(x) 10 11 for x in it2: 12 print(x) 13 14 """ 15 会输出 16 range(0, 5) 17 0 18 1 19 2 20 3 21 4 22 """
个人理解yield仅仅是截断并返回,而yield from相当于解析了后边的可迭代对象,并返回每一个item。
yield from iterable
本质上等于for item in iterable: yield item
的缩写版
注意:yield_from 后边的必须为可迭代对象(iterable)
3、 asyncio.cotoutine和yield from
yield from在asyncio模块中得以发扬光大。之前都是我们手工切换协程,现在当声明函数为协程后,我们通过事件循环来调度协程。
示例代码
1 import asyncio,random 2 @asyncio.coroutine 3 def smart_fib(n): 4 index = 0 5 a = 0 6 b = 1 7 while index < n: 8 sleep_secs = random.uniform(0, 0.2) 9 yield from asyncio.sleep(sleep_secs) #通常yield from后都是接的耗时操作 10 print('Smart one think {} secs to get {}'.format(sleep_secs, b)) 11 a, b = b, a + b 12 index += 1 13 14 @asyncio.coroutine 15 def stupid_fib(n): 16 index = 0 17 a = 0 18 b = 1 19 while index < n: 20 sleep_secs = random.uniform(0, 0.4) 21 yield from asyncio.sleep(sleep_secs) #通常yield from后都是接的耗时操作 22 print('Stupid one think {} secs to get {}'.format(sleep_secs, b)) 23 a, b = b, a + b 24 index += 1 25 26 if __name__ == '__main__': 27 loop = asyncio.get_event_loop() 28 tasks = [ 29 smart_fib(10), 30 stupid_fib(10), 31 ] 32 loop.run_until_complete(asyncio.wait(tasks)) 33 print('All fib finished.') 34 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对象(可以是生成器,迭代器)。
所以会报错:
yield from time.sleep(sleep_secs) TypeError: ‘NoneType’ object is not iterable
4、async和await
弄清楚了asyncio.coroutine和yield from之后,在Python3.5中引入的async和await就不难理解了:可以将他们理解成asyncio.coroutine/yield from的完美替身。当然,从Python设计的角度来说,async/await让协程表面上独立于生成器而存在,将细节都隐藏于asyncio模块之下,语法更清晰明了。
加入新的关键字 async ,可以将任何一个普通函数变成协程
mport time,asyncio,random async def mygen(alist): while len(alist) > 0: c = randint(0, len(alist)-1) print(alist.pop(c)) a = ["aa","bb","cc"] c=mygen(a) print(c) 输出: <coroutine object mygen at 0x02C6BED0>
在上面程序中,我们在前面加上async,该函数就变成一个协程了
但是async对生成器是无效的。async无法将一个生成器转换成协程。
还是刚才那段代码,我们把print改成yield
async def mygen(alist):
while len(alist) > 0:
c = randint(0, len(alist)-1)
yield alist.pop(c)
a = ["ss","dd","gg"]
c=mygen(a)
print(c)
# 可以看出:
<async_generator object mygen at 0x02AA7170>
可以看到输出:<async_generator object mygen at 0x02AA7170>
并不是coroutine 协程对象
所以我们的协程代码应该是这样的
import time,asyncio,random async def mygen(alist): while len(alist) > 0: c = random.randint(0, len(alist)-1) print(alist.pop(c)) await asyncio.sleep(1) strlist = ["ss","dd","gg"] intlist=[1,2,5,6] c1=mygen(strlist) c2=mygen(intlist) print(c1)
要注意的是await语法只能出现在通过async装饰的函数中否则会报SyntaxError错误, 而且await后边的对象需要是一个Awaitable,或者实现了相关的协议。
查看Awaitable抽象类的代码,表明了只要一个类实现了__await__方法,那么通过它构造出来的实例就是一个Awaitable:
要运行协程,要用事件循环
在上面的代码下面加上:
1 if __name__ == '__main__': 2 loop = asyncio.get_event_loop() 3 tasks = [ 4 c1, 5 c2 6 ] 7 loop.run_until_complete(asyncio.wait(tasks)) 8 print('All fib finished.') 9 loop.close()
就可以看到交替执行的效果。