python中和生成器協程相關的yield之最詳最強解釋,一看就懂(一)


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之最詳最強解釋,一看就懂(二)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM