標籤:

閑話yield,async以及await之間的關係

在描述複雜的async和await之前,我們先來看一下最簡單的函數調用形式:

def hello(): print(hello)

用Python的dis反編譯看一下hello函數:

2 0 LOAD_GLOBAL 0 (print)

2 LOAD_CONST 1 (hello)

4 CALL_FUNCTION 1

6 POP_TOP

8 LOAD_CONST 0 (None)

10 RETURN_VALUE

依據指令,先LOAD_GLOBAL,找到print函數,然後載入hello字元串,調用函數,彈出最上面的,載入常量None,返回常量。調用

hello()函數的時候,輸出的是hello字元串。

下面我在來看一下包含yield的hello函數是什麼樣的:

def hello_yield(): yield hello

也用dis看一下:

2 0 LOAD_CONST 1 (hello)

2 YIELD_VALUE

4 POP_TOP

6 LOAD_CONST 0 (None)

8 RETURN_VALUE

和上面的hello函數dis反編譯相比,主要差異在指令CALL_FUNCTION處被替換為YIELD_VALUE。當我們調用hello_yield()的時候,返回的是一個**generator對象**。注意,這個**函數內部**實際上並沒有調用,我們可以用下面的例子來驗證下:

def hello_yield2(): print(yield2) yield hello

當調用hello_yield2()函數時,並沒有列印yield2字元串,從側面也就表明了函數內部並沒有被調用到。

Python規定,當執行到yield語句時,被調用的函數掛起,將yield後面的表達式返回給調用方,然後下一次調用時再從掛起處繼續執行,當執行到函數尾部的時候,系統拋出StopIteration異常,如果函數有返回值,就會將返回值放到StopIteration異常中。

剛才我們看到,調用含有yield的函數,返回的是generator對象,那怎麼觸發函數內部調用呢?實際上每一個generator對象,會有四個方法,分別是__next__, send, throw, close。當__next__或send方法調用的時候,就會觸發函數內部調用,__next__通常是通過內置的next函數或者for語句來調用的,send函數就是顯式調用,並且send(None)和__next__是等價的。

所以

gen = hello_yield()result = next(gen)print(result)next(gen)

按照前面描述,上述第一次執行next的時候,就會執行到yield指令,函數掛起,返回hello。當第二次調用next的時候,執行到函數尾部了,所以會拋出StopIteration異常。

上面的函數調用,也等價與

gen = hello_yield()result = gen.send(None)print(result)gen.send(None)

send函數是可以將值發送給yield語句的,這使得generator對象的調用方和generator對象之間可以進行數據交換,如

def hello2(): v = yield hello print(v)gen = hello2()result = gen.send(None)print(result)gen.send(world)

第一次調用send(None)的時候,函數開始被執行,然後執行到yield的時候,返回hello字元串,並且掛起,然後第二次執行send(world)的時候,字元串將複製給v變數,然後繼續執行下去。亦即gen將字元串hello返回給了調用方,而調用方將world放進了gen內部的函數中。

而這種數據交換方式正是非同步編程的本質所在:一方gen獲取數據,然後將數據返回出去(yield hello)給外部,自己掛起等待,外部收到數據後,做出業務反應後(print(result)),再喚醒gen,gen收到業務結果(gen.send(hello))後,做後續業務處理(print(v)),直到業務結束。

如果我們將多個generator串聯起來,就會發現可以處理非常複雜的業務形式,下面是一個相對簡單的例子:

def hello(): v = yield hello print(v)def world(): v = yield world print(v)def task(): h = hello() rh = h.send(None) w = world() rw = w.send(None) print(%s %s %(rh, rw)) r = yield # only for suspend try: h.send(r) except StopIteration: pass try: w.send(r) except StopIteration: passt = task()t.send(None)t.send(outer)

t是一個generator對象,task函數內部的h和w都是一個generator對象,第一個t.send(None)的時候,執行到yield處,此時會列印hello world字元串。執行t.send(outer)的時候,r賦值為outer字元串,然後分別發送給h和w,h和w對應的函數內部分別執行print(v)後,t執行到尾部,拋出StopIteration異常。

顯然通過上面的yield和send來控制時,會發現代碼非常啰嗦,如果generator對象的調用鏈更長一些,就會發現需要大量調用send,需要更多的yield,以及不斷的處理StopIteration。

下面的例子,是將最內部獲取到的數據,返回給外部調用層,外部調用層處理數據結束後,再傳遞數據給最內層(這是最常見的一種非同步業務數據處理方式):

def hello(): v = yield hello print(v)def world(): v = yield world print(v)def task(): h = hello() rh = h.send(None) r = yield rh try: h.send(r) except StopIteration: pass w = world() rw = w.send(None) r = yield rw try: w.send(r) except StopIteration: passt = task()h = t.send(None)w = t.send(hello complete)print(%s %s %(h, w))t.send(stop)

上述代碼執行時,第一個t.send(None)的時候,調用到task的yield rh,然後h得到『hello字元串,第二次調用t.send(hello complete)時,r得到hello complete,然諾後通過h.send發送給h內部,h內部列印hello complete,然後執行到yield rw,w收到』world值,此時外部print函數執行,最後t.send(stop)時,將stop發送給w內部,列印stop,然後t拋出StopIteration。

顯然上面的代碼執行行為很複雜,為此Python引入了yield from語句來處理上面描述的業務邏輯,下面代碼的執行邏輯和前一個代碼執行邏輯基本一致:

def hello(): v = yield hello print(v)def world(): v = yield world print(v)def task(): h = hello() rh = yield from h w = world() rw = yield from wt = task()h = t.send(None)w = t.send(hello complete)print(%s %s %(h, w))v = t.send(stop)

整個task函數變得非常簡單,移除了send方法,也移除了StopIteration的處理,變得非常容易編寫。

注意:yield from是處理的generator的內部是可以有多個yield的。

def hello(): h = yield hello print(h, h) w = yield world print(w, w)def task(): yield from hello()t = task()h = t.send(None)w = t.send(h will print)print(%s %s %(h, w))t.send(w will print)

前面說過,send(None)是等價於__next__,所以task在for語句中執行的結果就等於循環調用send(None),如:

for t in task(): print(t)

輸出:

helloh Noneworldw None

進行__next__方法這樣的調用,對非同步編程並沒有什麼明顯的好處,而我們將生成器的迭代和非同步編程混在一起了,使得非同步編程和生成器之間非常容易混淆,容易出錯。

為了能夠區分,Python引入了await來替換yield from,對於一般的函數,我們為了便於區分非同步函數和普通生成器函數,Python引入了async,上面的例子,等價於下面的代碼:

import types@types.coroutinedef hello(): h = yield hello print(h, h) w = yield world print(w, w)async def task(): await hello()t = task()h = t.send(None)w = t.send(h will print)print(%s %s %(h, w))t.send(w will print)

types.coroutine修飾的generator,稱為generator-base coroutine,可以被await調用(類似yield from),await必須在async def函數中使用,而我們將async def定義的函數,稱為native coroutine。

推薦閱讀:

如何用python網路爬蟲求兩個城市間鐵路距離呢?
Python的from import和import的區別?
怎麼解決Python3亂碼問題?
你看好 Python 3 嗎?
python3下,re.findall返回值前後的[" 『]怎麼去掉?

TAG:Python | Python3x |