Python中的协程大概经历了如下三个阶段:
- 最初的生成器变形yield/send
- 引入@asyncio.coroutine和yield from
- 在最近的Python3.5版本中引入async/await关键字
一、生成器变形yield/send
In [31]: def gen():
...: value = 888168
...: while 1:
...: rece = yield value
...: if rece == 'e':
...: break
...: value = 'got: %s' % rece
...:
In [32]: g = gen()
In [33]: g.send(None) # 如何理解第一次的None?
Out[33]: 888168
In [34]: g.send('hello')
Out[34]: 'got: hello'
In [35]: g.send('e')
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-35-422bc441ae72> in <module>()
----> 1 g.send('e')
StopIteration:
这个协程很简单,关键是理解第一步None:
None在计算机中表示 无 的意思,说白了 send(None)并没有发送None,因为None就是啥都无,它的本意是启动这个生成器。好了,如何启动?
第一次启动是按顺序执行 gen() 的。先是执行value = 888168,然后继续,直到 rece = yield value
的 yield value部分停止。注意是 yield value部分停止。所以send(None)的结果才是888168。yield value 是暂停点,rece = yield是下次的启动点。
二、yield from
In [36]: def g1():
...: yield range(4) # range 中元素未释放出,你是无法 yield 的
In [37]: def g2():
...: yield from range(4) # 把 range 中元素释放出来再yield成一个生成器
In [38]: it1 = g1()
In [39]: it2 = g2()
In [40]: for x in it1: # 某种意思上等同一个生成器套娃,你一次 for in 不行的。
...: print(x)
...:
range(0, 4)
In [41]: for x in it2: # 释放生成器元素成果
...: print(x)
...:
0
1
2
3
这说明yield就是将range这个可迭代对象直接返回了。
而yield from解析了range对象,将其中每一个item返回了。
yield from iterable
本质上等于for item in iterable: yield item
的缩写版
来看一下例子,假设我们已经编写好一个斐波那契数列函数
In [42]: def fab(max): # 定义一个生成器
...: n,a,b = 0, 0, 1
...: while n < max:
...: yield b
...: a, b = b, a+ b
...: n = n + 1
In [43]: f = fab(4)
In [44]: f
Out[44]: <generator object fab at 0x1089814c0>
In [45]: def f_wrapper(fun_iterable):
...: print('start')
...: for item in fun_iterable: # for item 就是把生成器的元素给释放出来了
...: yield item # 但这里,yield又把释放的元素变成了生成器
...: print('end')
In [46]: wrap = f_wrapper(fab(5))
In [47]: for i in wrap: # 所以最后打印时候需要 for i in 来释放元素
...: print(i, end='')
...:
start
11235end # 结果就是1,1,2,3,5
In [48]: import logging
In [49]: def f_wrapper2(fun_iterable):
...: print('start')
...: yield from fun_iterable # 等同上方代码,把元素释放出来,重新yield后再组成新生成器
...: print('end')
In [50]: wrap = f_wrapper2( fab(5) )
In [52]: for i in wrap: # 释放元素
...: print(i, end='')
...:
start
11235end
yield from 后面必须跟iterable对象(可以是生成器,迭代器)
三、asyncio.coroutine和yield from
yield from在asyncio模块中得以发扬光大。之前都是我们手工切换协程,现在当声明函数为协程后,我们通过事件循环来调度协程。
先看示例代码
import asyncio,random
@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
@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
if __name__ == '__main__':
loop = asyncio.get_event_loop()
tasks = [
smart_fib(10),
stupid_fib(10),
]
loop.run_until_complete(asyncio.wait(tasks))
print('All fib finished.')
loop.close()
结果:
stupid one think 0.16154263754880197 secs to get 1 # stu花费0.16
stupid one think 0.05444056751627722 secs to get 1 # stu花费0.05 + 0.16 = 0.21
stupid one think 0.0021981642884256304 secs to get 2 # stu花费0.21 + 0.002 = 0.212
smart one think 0.37663551871717216 secs to get 1 # sma花费 0.376,随机生成的0.37>0.21,所以smart才在这时候开始执行
smart one think 0.2645472221109239 secs to get 1 # 这就是异步高效的秘密,谁快先执行
smart one think 0.15856033283994533 secs to get 2
stupid one think 0.6443246613879796 secs to get 3
stupid one think 0.02350589883775811 secs to get 5
smart one think 0.1801088550985621 secs to get 3
smart one think 0.3576929865189507 secs to get 5
stupid one think 0.569321733443974 secs to get 8
stupid one think 0.008074465892759975 secs to get 13
smart one think 0.33219791971546947 secs to get 8
stupid one think 0.23956673657999528 secs to get 21
stupid one think 0.011677097683787352 secs to get 34
stupid one think 0.06350886018228774 secs to get 55
smart one think 0.13317102581385262 secs to get 13
smart one think 0.21706707561261382 secs to get 21
smart one think 0.2992267724097271 secs to get 34
smart one think 0.24725642731182954 secs to get 55
all fib finished
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
四、async和await
弄清楚了asyncio.coroutine
和yield from
之后,在Python3.5中引入的async
和await
就不难理解了:可以将他们理解成asyncio.coroutine
/yield from
的完美替身。当然,从Python设计的角度来说,async
/await
让协程表面上独立于生成器而存在,将细节都隐藏于asyncio模块之下,语法更清晰明了。
加入新的关键字 async ,可以将任何一个普通函数变成协程
import 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)
输出: # 只是协程,不是异步,异步必须有 await
<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) # 单独的 async 与生成器是无法变成协程的,async 遇到生成器无法把函数变成协程
a = ["ss","dd","gg"]
c=mygen(a)
print(c)
可以看到输出
<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) # 非生成器函数,配合 asynic 成为协程,再配合 await 成为异步
strlist = ["ss","dd","gg"]
intlist=[1,2,5,6]
c1=mygen(strlist)
c2=mygen(intlist)
print(c1)
要运行协程,要用事件循环
在上面的代码下面加上:
if __name__ == '__main__':
loop = asyncio.get_event_loop()
tasks = [ # 事件循环
c1,
c2
]
loop.run_until_complete(asyncio.wait(tasks))
print('All fib finished.')
loop.close()
就可以看到为追求高效率的交替执行的效果。
Python黑魔法 --- 异步IO( asyncio) 协程
网友评论