4. 生成器(generator)
4.1. 生成器簡介
首先請確信,生成器就是一種迭代器。生成器擁有next方法並且行為與迭代器完全相同,這意味著生成器也可以用於Python的for迴圈中。另外,對於生成器的特殊語法支援使得編寫一個生成器比自定義一個常規的迭代器要簡單不少,所以生成器也是最常用到的特性之一。
從Python 2.5開始,[PEP 342:通過增強生成器實現協同程式]的實現為生成器加入了更多的特性,這意味著生成器還可以完成更多的工作。這部分我們會在稍後的部分介紹。
4.2. 生成器函式
4.2.1. 使用生成器函式定義生成器
如何獲取一個生成器?首先來看一小段程式碼:
1 2 3 4 5 6 7 |
>>> def get_0_1_2(): ... yield 0 ... yield 1 ... yield 2 ... >>> get_0_1_2 <function get_0_1_2 at 0x00B2CB70> |
我們定義了一個函式get_0_1_2,並且可以檢視到這確實是函式型別。但與一般的函式不同的是,get_0_1_2的函式體內使用了關鍵字yield,這使得get_0_1_2成為了一個生成器函式。生成器函式的特性如下:
呼叫生成器函式將返回一個生成器;
1 2 3 |
>>> generator = get_0_1_2() >>> generator <generator object get_0_1_2 at 0x00B1C7D8> |
第一次呼叫生成器的next方法時,生成器才開始執行生成器函式(而不是構建生成器時),直到遇到yield時暫停執行(掛起),並且yield的引數將作為此次next方法的返回值;
1 2 |
>>> generator.next() 0 |
之後每次呼叫生成器的next方法,生成器將從上次暫停執行的位置恢復執行生成器函式,直到再次遇到yield時暫停,並且同樣的,yield的引數將作為next方法的返回值;
1 2 3 4 |
>>> generator.next() 1 >>> generator.next() 2 |
如果當呼叫next方法時生成器函式結束(遇到空的return語句或是到達函式體末尾),則這次next方法的呼叫將丟擲StopIteration異常(即for迴圈的終止條件);
1 2 3 4 |
>>> generator.next() Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration |
生成器函式在每次暫停執行時,函式體內的所有變數都將被封存(freeze)在生成器中,並將在恢復執行時還原,並且類似於閉包,即使是同一個生成器函式返回的生成器,封存的變數也是互相獨立的。
我們的小例子中並沒有用到變數,所以這裡另外定義一個生成器來展示這個特點:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
>>> def fibonacci(): ... a = b = 1 ... yield a ... yield b ... while True: ... a, b = b, a+b ... yield b ... >>> for num in fibonacci(): ... if num > 100: break ... print num, ... 1 1 2 3 5 8 13 21 34 55 89 |
看到while True可別太吃驚,因為生成器可以掛起,所以是延遲計算的,無限迴圈並沒有關係。這個例子中我們定義了一個生成器用於獲取斐波那契數列。
4.2.2. 生成器函式的FAQ
接下來我們來討論一些關於生成器的有意思的話題。
1. 你的例子裡生成器函式都沒有引數,那麼生成器函式可以帶引數嗎?
當然可以啊親,而且它支援函式的所有引數形式。要知道生成器函式也是函式的一種:)
1 2 3 4 5 |
>>> def counter(start=0): ... while True: ... yield start ... start += 1 ... |
這是一個從指定數開始的計數器。
2. 既然生成器函式也是函式,那麼它可以使用return輸出返回值嗎?
不行的親,是這樣的,生成器函式已經有預設的返回值——生成器了,你不能再另外給一個返回值;對,即使是return None也不行。但是它可以使用空的return語句結束。如果你堅持要為它指定返回值,那麼Python將在定義的位置贈送一個語法錯誤異常,就像這樣:
1 2 3 4 5 6 |
>>> def i_wanna_return(): ... yield None ... return None ... File "<stdin>", line 3 SyntaxError: 'return' with argument inside generator |
3. 好吧,那人家需要確保釋放資源,需要在try…finally中yield,這會是神馬情況?(我就是想玩你)我在finally中還yield了一次!
Python會在真正離開try…finally時再執行finally中的程式碼,而這裡遺憾地告訴你,暫停不算哦!所以結局你也能猜到吧!
1 2 3 4 5 6 7 8 9 10 11 |
>>> def play_u(): ... try: ... yield 1 ... yield 2 ... yield 3 ... finally: ... yield 0 ... >>> for val in play_u(): print val, ... 1 2 3 0 |
*這與return的情況不同。return是真正的離開程式碼塊,所以會在return時立刻執行finally子句。
*另外,“在帶有finally子句的try塊中yield”定義在PEP 342中,這意味著只有Python 2.5以上版本才支援這個語法,在Python 2.4以下版本中會得到語法錯誤異常。
4. 如果我需要在生成器的迭代過程中接入另一個生成器的迭代怎麼辦?寫成下面這樣好傻好天真。。
1 2 3 4 5 |
>>> def sub_generator(): ... yield 1 ... yield 2 ... for val in counter(10): yield val ... |
這種情況的語法改進已經被定義在[PEP 380:委託至子生成器的語法]中,據說會在Python 3.3中實現,屆時也可能回饋到2.x中。實現後,就可以這麼寫了:
1 2 3 4 5 6 7 8 |
>>> def sub_generator(): ... yield 1 ... yield 2 ... yield from counter(10) File "<stdin>", line 4 yield from counter(10) ^ SyntaxError: invalid syntax |
看到語法錯誤木有?現在我們還是天真一點吧~
有更多問題?請回復此文:)
4.3. 協同程式(coroutine)
協同程式(協程)一般來說是指這樣的函式:
- 彼此間有不同的區域性變數、指令指標,但仍共享全域性變數;
- 可以方便地掛起、恢復,並且有多個入口點和出口點;
- 多個協同程式間表現為協作執行,如A的執行過程中需要B的結果才能繼續執行。
協程的特點決定了同一時刻只能有一個協同程式正在執行(忽略多執行緒的情況)。得益於此,協程間可以直接傳遞物件而不需要考慮資源鎖、或是直接喚醒其他協程而不需要主動休眠,就像是內建了鎖的執行緒。在符合協程特點的應用場景,使用協程無疑比使用執行緒要更方便。
從另一方面說,協程無法併發其實也將它的應用場景限制在了一個很狹窄的範圍,這個特點使得協程更多的被拿來與常規函式進行比較,而不是與執行緒。當然,執行緒比協程複雜許多,功能也更強大,所以我建議大家牢牢地掌握執行緒即可:Python執行緒指南
這一節裡我也就不列舉關於協程的例子了,以下介紹的方法瞭解即可。
Python 2.5對生成器的增強實現了協程的其他特點,在這個版本中,生成器加入瞭如下方法:
1. send(value):
send是除next外另一個恢復生成器的方法。Python 2.5中,yield語句變成了yield表示式,這意味著yield現在可以有一個值,而這個值就是在生成器的send方法被呼叫從而恢復執行時,呼叫send方法的引數。
1 2 3 4 5 6 7 8 9 10 |
>>> def repeater(): ... n = 0 ... while True: ... n = (yield n) ... >>> r = repeater() >>> r.next() 0 >>> r.send(10) 10 |
*呼叫send傳入非None值前,生成器必須處於掛起狀態,否則將丟擲異常。不過,未啟動的生成器仍可以使用None作為引數呼叫send。
*如果使用next恢復生成器,yield表示式的值將是None。
2. close():
這個方法用於關閉生成器。對關閉的生成器後再次呼叫next或send將丟擲StopIteration異常。
3. throw(type, value=None, traceback=None):
這個方法用於在生成器內部(生成器的當前掛起處,或未啟動時在定義處)丟擲一個異常。
*別為沒見到協程的例子遺憾,協程最常見的用處其實就是生成器。
4.4. 一個有趣的庫:pipe
這一節裡我要向諸位簡要介紹pipe。pipe並不是Python內建的庫,如果你安裝了easy_install,直接可以安裝它,否則你需要自己下載它:http://pypi.python.org/pypi/pipe
之所以要介紹這個庫,是因為它向我們展示了一種很有新意的使用迭代器和生成器的方式:流。pipe將可迭代的資料看成是流,類似於linux,pipe使用’|’傳遞資料流,並且定義了一系列的“流處理”函式用於接受並處理資料流,並最終再次輸出資料流或者是將資料流歸納得到一個結果。我們來看一些例子。
第一個,非常簡單的,使用add求和:
1 2 3 |
>>> from pipe import * >>> range(5) | add 10 |
求偶數和需要使用到where,作用類似於內建函式filter,過濾出符合條件的元素:
1 2 |
>>> range(5) | where(lambda x: x % 2 == 0) | add 6 |
還記得我們定義的斐波那契數列生成器嗎?求出數列中所有小於10000的偶數和需要用到take_while,與itertools的同名函式有類似的功能,擷取元素直到條件不成立:
1 2 3 4 5 |
>>> fib = fibonacci >>> fib() | where(lambda x: x % 2 == 0)\ ... | take_while(lambda x: x < 10000)\ ... | add 3382 |
需要對元素應用某個函式可以使用select,作用類似於內建函式map;需要得到一個列表,可以使用as_list:
1 2 |
>>> fib() | select(lambda x: x ** 2) | take_while(lambda x: x < 100) | as_list [1, 1, 4, 9, 25, 64] |
pipe中還包括了更多的流處理函式。你甚至可以自己定義流處理函式,只需要定義一個生成器函式並加上修飾器Pipe。如下定義了一個獲取元素直到索引不符合條件的流處理函式:
1 2 3 4 5 6 |
>>> @Pipe ... def take_while_idx(iterable, predicate): ... for idx, x in enumerate(iterable): ... if predicate(idx): yield x ... else: return ... |
使用這個流處理函式獲取fib的前10個數字:
1 2 |
>>> fib() | take_while_idx(lambda x: x < 10) | as_list [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] |
更多的函式就不在這裡介紹了,你可以檢視pipe的原始檔,總共600行不到的檔案其中有300行是文件,文件中包含了大量的示例。
pipe實現起來非常簡單,使用Pipe裝飾器,將普通的生成器函式(或者返回迭代器的函式)代理在一個實現了__ror__方法的普通類例項上即可,但是這種思路真的很有趣。
函數語言程式設計指南全文到這裡就全部結束了,希望這一系列文章能給你帶來幫助。希望大家都能看到一些結構式程式設計之外的程式設計方式,並且能夠熟練地在恰當的地方使用 :)
明天我會整理一個目錄放上來方便檢視,並且列出一些供參考的文章。遺憾的是這些文章幾乎都是英文的,請努力學習英語吧 – -#