Python 上篇:3- Python 是如何從 yield-send 到 yield from 再到 async-await

Python 上篇

  1. Python 是如何從 yield/send 到 yield from 再到 async/await

Python 中的協程大概經歷瞭如下三個階段:

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

  2. 引入 @asyncio.coroutine 和 yield from

  3. 在最近的 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