1. 程式人生 > >Python中的生成器和協程

Python中的生成器和協程

摘要:

今天讀了《A Curious Course on Coroutines and Concurrency》的,以下為我的筆記。
1. 生成器和協程的異同
2. 協程的一些特性

生成器和協程的異同

今天看過這本書以後,對於生成器和協程的理解突然增加了不少,特寫與此,以備記錄。

生成器和協程都是通過python中的yield的關鍵字實現的,不同的是,生成器只會呼叫next來不斷地生成資料,而協程卻會呼叫nextsend來返回結果和接收引數。

作者還一再地強調,儘管生成器和協程看起來很像,但是它們代表的卻是完全不同的設計理念。生成器是用來生成資料的,而協程從某種意義上來說是消耗資料的,而且作者還一再地強調,協程和迭代無關

,儘管協程也會用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的異常,而這個異常在協程內部被捕獲到了!

參考文獻