用yield實現協程

用yield實現協程

來自專欄 Python與數據分析

上一篇 理解python中的yield關鍵字 介紹了使用yeld實現生成器函數,這一篇我們來繼續深入的了解一下yield,用yield實現協程。

先來解答一下上一篇留下的問題:下面的代碼為什麼第二次調用next列印None呢?

def echo(n): while True: n = yield ng = echo(1)print(next(g))print(next(g))

事實是這樣的,yield語句默認返回None。當第一次調用next方法時,生成器函數開始執行,執行到yield表達式為止,但此時賦值操作並為執行。上面的代碼中,在第一次調用next的時候,echo生成了1。第二次調用next的時候,yield表達式的值賦給了n,n此時變成None了,再次yield n的時候就自然生成None了。

好了,接下來開始本文的主題。

什麼是協程

引用官方的說法:

協程是一種用戶態的輕量級線程,協程的調度完全由用戶控制。協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧,直接操作棧則基本沒有內核切換的開銷,可以不加鎖的訪問全局變數,所以上下文的切換非常快。

與線程相比,協程更輕量。一個Python線程大概佔用8M內存,而一個協程只佔用1KB不到內存。協程更適用於IO密集型的應用。

在講述協程的實現前,我們有必要先來看一下send方法。

send方法

yield表達式有一個返回值,send方法的作用就是控制這個返回值,send的參數就是yield表達式的返回值。我們來看一下官方文檔上關於send的定義

generate.send(value):

生成器的send(value)方法會將value值「發送」給生成器中的方法。value參數變成當前yield表達式的值。send()方法會返回生成器生成的下一個yield值或者StopIteration異常(如果生成器沒有生成下一個yield值就退出了)。當通過調用send()啟動生成器時,value值必須為None,因為當前還沒有yield表達式可以接收參數。

是不是看暈了?我們來看一個例子

def func(): while True: print("before yield") x = yield print("after yield:",x)g = func()next(g) # 程序運行到yield並停在該處,等待下一個nextg.send(1) # 給yield發送值1,這個值被賦值給了x,並且列印出來,然後繼續下一次循環停在yield處g.send(2) # 給yield發送值2,這個值被賦值給了x,並且列印出來,然後繼續下一次循環停在yield處next(g) # 沒有給x賦值,執行print語句,列印出None,繼續循環停在yield處

上面的代碼輸出

before yieldafter yield: 1before yieldafter yield: 2before yieldafter yield: Nonebefore yield

第一次調用next的時候,程序從函數最開始處運行,列印出

before yield

執行到yield處,停在該處。

接下來,向生成器send(1)。send在這裡起到兩個作用,一個是將參數值賦給yield的返回值,然後該返回值賦給了變數x;一個是繼續程序的執行,直到下一次遇到yield停下來。第二個功能和next類似。其實,next 就相當於 send(None) 。

執行了 send(1) 後,x被賦值給yield的返回值,即send的參數1,並繼續往下執行,列印出了

after yield: 1

繼續執行,回到循環的開始,向下執行,列印出

before yield

再次遇到yield,停在該處,等待下一次send或next的調用。

向生成器send(2)。這裡的步驟和 send(1) 相同,列印出下面兩條後,在yield處停住。

after yield: 2

before yield

執行 next(g),x被賦值為yield表達式的返回值,也就是None,繼續向下執行,列印出

after yield: None

再次回到循環的開始,向下執行,列印出

before yield

程序運行結束。

現在是不是有點理解send了?

yield和send實現Python協程

我們來用協程實現一個生產者/消費者的例子

import timedef consume(): r = while True: n = yield r if not n: return print([consumer] consuming %s... % n) time.sleep(1) r = well receiveddef produce(c): next(c) n = 0 while n < 5: n = n + 1 print([producer] producing %s... % n) r = c.send(n) print([producer] consumer return: %s % r) c.close()if __name__==__main__: c = consume() produce(c)

運行上面的程序,會輸出

[producer] producing 1...[consumer] consuming 1...[producer] consumer return: well received[producer] producing 2...[consumer] consuming 2...[producer] consumer return: well received[producer] producing 3...[consumer] consuming 3...[producer] consumer return: well received[producer] producing 4...[consumer] consuming 4...[producer] consumer return: well received[producer] producing 5...[consumer] consuming 5...[producer] consumer return: well received

produce函數負責生產數據,consume函數負責消費數據。具體執行過程如下:

  1. 首先調用consume函數,consume函數的返回是一個生成器,把這個生成器傳入produce函數。
  2. produce函數中調用next(c)啟動生成器。
  3. 計算 n = n+1 生成數據,一旦生產了數據,調用 c.send(n) 切換到consume執行。
  4. consume函數中拿到數據後賦值給n,繼續執行yield後面的語句。
  5. consume函數中列印消費的數據,並設置返回值r,又回到循環的開始,通過yield把結果傳回。
  6. produce拿到consume返回的值,繼續生產下一個數據。
  7. 5個數據生產完畢後,循環結束,通過c.close()關閉consume,結束全過程。

produce和consume函數在一個線程內執行,通過調用send方法和yield互相切換,實現協程的功能。

本文已更新微信同名公眾號【Python與數據分析】,歡迎關注~


推薦閱讀:

TAG:Python | 協程 | Python開發 |