yield是python中一個很是重要的關鍵詞,全部迭代器都是yield實現的,學習python,若是不把這個yield的意思和用法完全搞清楚,學習python的生成器,協程和異步io的時候,就會完全懵逼。因此寫一篇總結講講yield的東西。html
分紅四塊來說, 這篇先說yield基本用法,後面會重點將yield from的牛逼之處python
一, 生成器中使用yield異步
語法形式:yield <表達式>函數
這種狀況,能夠簡單的把它理解爲 return <表達式>, 每次next調用,會觸發生成器從上一次中止的地方開始執行, 直到下一次遇到yield。 若是是第一次next,就是那個從函數開始知道第一個yield;之後每次next,則都是從上次yiled中止的地方開始, 繼續執行,直到遇到下一次yiled, 若是一直執行到函數結束,都沒有下個yield遇到, 則生成器運行終止,此時next會在生成器運行終止時,拋出StopIteration異常。 好比下面的代碼學習
1 def generator(): 2 print("gen-0: start") 3 for i in range(2): 4 print("gen-%s-a: i=%s" % (i,i)) 5 yield i + 1 # 可簡單理解爲 return i + 1, 並等待 6 print("gen-%s-b: i=%s" % (i,i)) 7 print("gen-c") 8 yield 3 # 可簡單理解爲 return 3, 並等待 9 print("gen-d") # 此處繼續執行完成後,因爲後續在無其它yield語句,調用next的代碼會拋出StopIteration異常 10 11 try: 12 g = generator() # 因爲generator是一個包含yield關鍵字的生成器,此處直接調用gengerator()只是返回該生成器實例,實際無任何輸出 13 print('main-0: start') 14 n = next(g) # next觸發生成器執行,因爲是第一次觸發,因此從generator循環外第2行和第4行的打印會輸出,到第5行遇到yield後返回i+1到主程序,第6行不會執行 15 print('main-1: n=%s' % n) 16 n = next(g) # next第二次觸發生成器執行,因爲是第二次觸發,上次執行到第5行, 此時從第6行開始執行, 將輸出第6行和第4行的打印, 並再次在第5行返回 17 print('main-2: n=%s' % n) 18 n = next(g) # next第三次觸發生成器執行,此次generator內部for循環已經完成,故執行完跳出循環執行到第8行後返回 19 print('main-3: n=%s' % n) 20 n = next(g) # next視圖第四次觸發生成器執行,從第9行開始執行,但此後generator已經所有執行完成,因此此時next會拋出StopIteration異常,賦值沒法完成 21 print('main-5: n=%s' % n) # 異常產生, 此行將永遠不會被執行 22 except StopIteration: 23 print('StopIteration n=%s' % n) # 第17行的next將觸發異常
上面這段代碼執行輸出以下, 各位看官自行對照代碼和詳細註釋很容易看懂spa
main-0: start
gen-0: start
gen-0-a: i=0
main-1: n=1
gen-0-b: i=0
gen-1-a: i=1
main-2: n=2
gen-1-b: i=1
gen-c
main-3: n=3
gen-d
StopIteration n=3 code
二, 能夠接收傳入參數值的yield協程
語法形式:x = yield <表達式>htm
next方式觸發生成器沒法傳入參數, 若是想觸發generator的同時, 傳入參數給生成器,是否能夠喃?答案是能夠的, 這時候須要作兩個改變,一是在生成器內增長變量接收該參數, 即本屆標題形式;二是外部調用時, 不使用next, 而採用用調用生成器的send方法便可blog
1 def coroutine(): # 這個生成器,咱們其名coroutine, 意思是協程,協程和生成器都是使用yield來實現, 但本質上不一個概念 2 print("coroutine: start") 3 for i in range(2): 4 print("coroutine-a: i=%s" % i) 5 x = yield i + 1 # 由send傳入的參數值將被賦值給x, 注意i+1的值是被yield用來返回的,不會賦值給x ! 6 print("coroutine-b: i=%s, x=%s" % (i,x)) 7 8 cr = coroutine() # 建立一個生成器實例 9 next(cr) # 生成器的第一次觸發必須使用next,此時若是試圖用send發送參數,將致使異常敗,緣由是此時生成器還未啓動 10 try: 11 print("main-a:") 12 y = cr.send(0) # 調用生成器的send方法, 將10傳給生成器, 並觸發一次生成器的執行 13 print("main-b: y=%s" % y) 14 y = cr.send(1) # 調用生成器的send方法, 將1傳給生成器, 並觸發一次生成器的執行 15 print("main-c: y=%s" % y) # 14行的消息發送將致使StopIteration異常產生, 此行將永遠不會被執行 16 except StopIteration: 17 print("StopIteration")
如下是執行後的輸出
coroutine: start coroutine-a: i=0 main-a: coroutine-b: i=0, x=0 coroutine-a: i=1 main-b: y=2 coroutine-b: i=1, x=1 StopIteration
簡單再解釋一下,
1. 第一次執行next時,生成器經過yield i + 1 返回1, 並停在第5行等待被喚醒, 此時賦值動做還沒有發生, x的值只是個None, 同時也爲執行後面的print。
2. 生成器經過next進行過首次觸發後,能夠用send發送參數,好比第11行和12行,生成器內部變量先分別被賦值爲 0和1, 你們能夠對照輸出看
3. 因爲生成器總共只有兩次yield的機會, next消耗一次,第一次send消耗一次,因此第二次send後,雖然不影響執行完參數傳遞給生成器的動做, 但因爲生成器自身找不下一次yield的機會,生成器執行終止。 致使send拋出StopIteration異常
三。生成器自己的返回值
不知道你們注意到沒有, 直接相似的cr = coroutine() 的調用,只是產生一個生成器實例,並沒執行。而經過yield的返回時, 生成器自己的邏輯實際上都是爲走完的, yield英文是退讓的意思,除了無限序列的生成器,生成器最終都會執行完成,那麼此時若是生成器經過return正常返回一個值,生成器的使用者能得到嗎 ?好比下面的代碼, 生成器正常結束後return的代碼, 使用方如何得到喃 ?好比下面代碼如何獲取第10行正常終止後的返回值10
1 def generator(): 2 yield 5 3 return 10 # 這個返回值如何得到 ? 4 5 cr = generator() 6 n = next(cr) # yield 5返回的值能夠被得到並賦值給n
答案是在StopIteration異常的處理邏輯中能夠得到,方法以下, 只須要在上面代碼後面繼續加上如下處理
try: next(cr) except StopIteration as e: # 經過except ... as 將異常保存在變量e中 rt = e.value # 異常變量e的value值就是生成器經過return返回值
嚴格意義上講, python將StopIteration定義爲一個異常多是不得已的事情, 由於這個異常的意思, 實際就是告訴使用者, 「我沒發生成下一個了, yield次數已經用完了, 個人使命結束了」, 從這個意義上講, StopIteration也能夠歸爲是正常邏輯, 因此強烈建議全部使用用生成器的地方,都應該要加上StopIteration異常處理。
四,不用next觸發生成器執行
生成器的首次觸發必定要使用next或者send(None)觸發,這個有時候比較麻煩, 有沒有不須要寫next的狀況喃。 答案是確定的。咱們能夠把generator放在循環裏面,好比把第一個例子的幾回next改爲一個for循環
def generator(): print("gen-0: start") for i in range(2): yield i + 1 yield 3 try: g = generator() for n in g: # for循環是經過next觸發生成器實現的, 內部會調用next(g) print(n) except StopIteration: print('StopIteration n=%s' % n)
將獲得下面的輸出, 由於在python裏面全部的迭代,包括循環語句,實際底層的實現都是經過生成器搞定的, 因此在for循環裏面, 其實是python內部實如今幫你調用next來觸發生成器.和咱們在第一例中的顯示依次調用next(g)是同樣的 能夠看到第一次打印了「start」輸出, 後面依次輸出1,2,3. 但沒有StopExeption拋出, 那是由於for的語意是循環次數和實際相同, 因此最後一次next被for內部消化了, 沒有暴露出來而已
gen-0: start 1
2
3
四。 再說 x = yield i, 生成器和協程
咱們在來看一下上面第二部分的例子, 這裏的x = yield i + 1語句, 實際上使函數自己同時具有了生產者和消費的功能, yield i + 1會讓每次執行產生一個值, 這是生成者,而 x = yiled又讓它能夠接收一個send所發送的值。這樣看起來同時具有了雙重功能, 但這倒是一種很差的用法, 應該儘可能避免。而是讓一個函數功能單一, 要嗎做爲生產器,要嗎做爲僅具有消費功能的協程。雖然協程和生成器都是用yield來實現的, 但不該該將兩者功能混淆, 這可能會致使一些難以理解的代碼,是不推薦這麼用的。 推薦的用發是分開,要嗎做爲生成器,要嗎做爲協程,不要讓一個函數同時兼備兩者的功能
第一節是做爲生成器的例子
做爲協程, 建議的方式是使用 x = (yield), 不要讓yiled生成任何值, 僅僅用於接收
def coroutine(): # 這個是協程,消費每次收到的值 print("coroutine: start") While True: print("coroutine-a: i=%s" % i) x = (yield) print("coroutine-b: x=%s" % x)