Python中的生成器和協程
摘要:
今天讀了《A Curious Course on Coroutines and Concurrency》的,以下為我的筆記。
1. 生成器和協程的異同
2. 協程的一些特性
生成器和協程的異同
今天看過這本書以後,對於生成器和協程的理解突然增加了不少,特寫與此,以備記錄。
生成器和協程都是通過python中的yield
的關鍵字實現的,不同的是,生成器只會呼叫next
來不斷地生成資料,而協程卻會呼叫next
和send
來返回結果和接收引數。
作者還一再地強調,儘管生成器和協程看起來很像,但是它們代表的卻是完全不同的設計理念。生成器是用來生成資料的,而協程從某種意義上來說是消耗資料的,而且作者還一再地強調,協程和迭代無關
next
來獲取資料,但是協程和迭代無關,不要嘗試像使用生成器那樣去迭代地使用協程。
個人理解就是生成器是通過迭代來不斷地獲取資料的一個東西。而協程呢,根本和生成器沒有半毛錢關係(儘管它們都用yield
),我在協程的維基百科裡面看到,協程是和子例程(也就是程式語言中的函式)比較著說的。
- 子例程呼叫完了就結束了,但是協程
yield
返回後並沒有結束,只要你願意,可以無限呼叫下去 - 子例程只有一個入口(引數)和一個出口(返回值),但是對於協程,一個
yield
就是一個入口或者出口,也就是說,協程可以擁有任意多的入口和出口 - 子例程之間是相互呼叫的關係(函式a呼叫函式b),但是協程之間是平等的關機,通過
yield
這就是協程,用維基百科的話來說,就是和子例程一樣,也是一種程式元件。
關於協程
除了協程和生成器的比較外,還看到書中講了一些關於協程的一些比較有意思的東西,特在此寫出來,以備查閱。
協程的啟動
先舉個例子,比如下面這個模擬Unix grep的協程:
import re
def grep(pattern):
pattern = re.compile(pattern)
while True:
line = (yield)
m = pattern.search(line)
if m:
print(m.string)
g = grep(r'^abcd' )
g.send('abcd')
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-14-6248a9077ec9> in <module>()
12
13 g = grep(r'^abcd')
---> 14 g.send('abcd')
TypeError: can't send non-None value to a just-started generator
如上所示,我們構造了一個協程g
,如果我們直接向其中傳送查詢字串,就會丟擲一個TypeError
,顯示cann't send non-None value to a just-started generator
。也就是說,我們需要先啟動協程。其實這個啟動過程,就是讓上面那個函式先執行,執行到yield
處,然後這個協程才能通過send
來接收值。
那麼如何啟動協程呢,其實也很簡單,只需要執行next(g)
或者g.send(None)
就可以了。
但是,每次都這樣手動地去啟動協程,太容易忘掉了,我們可以去寫一個裝飾器,加到協程函式上,讓其自動啟動,程式碼如下所示:
import re
def coroutine(func):
def start(*args, **kwargs):
cr = func(*args, **kwargs)
next(cr)
return cr
return start
@coroutine
def grep(pattern):
pattern = re.compile(pattern)
while True:
line = (yield)
m = pattern.search(line)
if m:
print(m.string)
g = grep(r'^abcd')
g.send('abcd') # True
g.send('1234abcd') # False
abcd
協程的關閉
接下來我們再來說說協程的關閉,還以上面的那個grep
協程為例子,由於它的yield
語句是寫在一個死迴圈裡面的,所以只要我們一直send
,這個協程就會一直執行下去,那麼該如何停止這個協程呢,其實也很簡單,只要呼叫協程的close
函式即可,如下所示:
import re
def coroutine(func):
def start(*args, **kwargs):
cr = func(*args, **kwargs)
next(cr)
return cr
return start
@coroutine
def grep(pattern):
pattern = re.compile(pattern)
while True:
line = (yield)
m = pattern.search(line)
if m:
print(m.string)
g = grep(r'^abcd')
g.send('abcd')
g.close()
g.send('1abcd')
abcd
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-9-1958b0399f9e> in <module>()
22 g.send('abcd')
23 g.close()
---> 24 g.send('1abcd')
StopIteration:
從上面的程式碼可以看出,當我們關閉了協程以後,如果再通過send
向其中傳送值的話,就會丟擲一個StopIteration
異常了。
需要注意的是,close
函式其實是向協程內部丟擲了一個GeneratorExit
異常,我們當然也可以捕獲這個異常,不過就算捕獲了這個異常,協程一樣會退出,而且對於這個異常唯一合理的做法就是清理環境並退出。
向協程丟擲異常
除了可以向協程中傳送值以外,也可以通過throw
函式向協程中丟擲異常,而這個異常像普通的異常一樣,也可以通過try-except
來捕獲,請看下面這段程式碼:
import re
def coroutine(func):
def start(*args, **kwargs):
cr = func(*args, **kwargs)
next(cr)
return cr
return start
@coroutine
def grep(pattern):
pattern = re.compile(pattern)
while True:
try:
line = (yield)
except RuntimeError as e:
print('I catch you |%s| haha!' % e)
continue
m = pattern.search(line)
if m:
print(m.string)
g = grep(r'^abcd')
g.send('abcd')
g.throw(RuntimeError, "You can't catch me!")
abcd
I catch you |You can't catch me!| haha!
在上面的程式碼中,我們通過throw
函式向協程內部丟擲了一個RuntimeError
的異常,而這個異常在協程內部被捕獲到了!