yield是python中一個非常重要的關鍵詞,所有迭代器都是yield實現的,學習python,如果不把這個yield的意思和用法徹底搞清楚,學習python的生成器,協程和異步io的時候,就會徹底懵逼。所以寫一篇總結講講yield的東西。
分成四塊來講, 這篇先說yield基本用法,后面會重點將yield from的牛逼之處
一, 生成器中使用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將觸發異常
上面這段代碼執行輸出如下, 各位看官自行對照代碼和詳細注釋很容易看懂
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
二, 可以接收傳入參數值的yield
語法形式:x = yield <表達式>
next方式觸發生成器無法傳入參數, 如果想觸發generator的同時, 傳入參數給生成器,是否可以喃?答案是可以的, 這時候需要做兩個改變,一是在生成器內增加變量接收該參數, 即本屆標題形式;二是外部調用時, 不使用next, 而采用用調用生成器的send方法即可
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)
下一篇 : python中和生成器協程相關的yield from之最詳最強解釋,一看就懂(二)