Python 上篇:3- Python 是如何從 yield-send 到 yield from 再到 async-await
Python 上篇
- Python 是如何從 yield/send 到 yield from 再到 async/await
Python 中的協程大概經歷瞭如下三個階段:
最初的生成器變形 yield/send
引入 @asyncio.coroutine 和 yield from
在最近的 Python3.5 版本中引入 async/await 關鍵字
1. 生成器 yield/send
生成器就是一種迭代器,可以使用 for 進行迭代。生成器函數最大的特點是可以接受外部傳入的一個變量,並根據變量內容計算結果後返回。這一切都是靠生成器內部的 send() 函數實現的。
def gen():
value=0
while True:
receive=yield value
if receive=='e':
break
value = 'got: %s' % receive
g=gen()
print(g.send(None))
print(g.send('hello'))
print(g.send(123456))
print(g.send('e'))
上面生成器函數中最關鍵也是最易理解錯的,就是 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 會輸出”got: hello”,並等待 send() 激活。
3、通過 g.send(123456),會重複第 2 步,最後輸出結果爲”got: 123456″。
4、當我們 g.send(‘e’) 時,程序會執行 break 然後推出循環,最後整個函數執行完畢,所以會得到 StopIteration 異常。
從上面可以看出, 在第一次 send(None) 啓動生成器(執行 1–>2,通常第一次返回的值沒有什麼用)之後,對於外部的每一次 send(),生成器的實際在循環中的運行順序是 3–>1–>2,也就是先獲取值,然後 dosomething,然後返回一個值,再暫停等待。
2. yield from
def g1():
yield range(5)
def g2():
yield from range(5)
it1 = g1()
it2 = g2()
for x in it1:
print(x)
for x in it2:
print(x)
輸出:
range(0, 5) //g1
//g2
0
1
2
3
4
這說明 yield 就是將 range 這個可迭代對象直接返回了。
而 yield from 解析了 range 對象,將其中每一個 item 返回了。
yield from iterable 本質上等於 for item in iterable: yield item 的縮寫版。
再強調一遍:yield from 後面必須跟 iterable 對象 (可以是生成器,迭代器),(生成器一定是迭代器 iterator,迭代器一定是可迭代對象 iterable)
3. 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()
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
python3.5 之後就開始原生支持協稱了,你只需要在函數開頭以 async 開始,在需要等待的地方 await 就行,這樣就完完全全實現了協稱,創建示例如下:
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)
運行協程,要用事件循環 在上面的代碼下面加上:
if __name__ == '__main__':
loop = asyncio.get_event_loop()
tasks = [
c1,
c2
]
loop.run_until_complete(asyncio.wait(tasks))
print('All fib finished.')
loop.close()
就可以看到交替執行的效果:
5. 小結
這篇文章我們講解了協稱經歷的三個階段,這三個階段可以說是 python 支持異步編程必經之路,當所有語言都支持協稱的時候,你不支持,很有可能就會失去很大一塊市場,所以 python 爲了支持也是再艱難的前行啊。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/H0crNAarU29xb1wICztiDQ