
本文將介紹Python中函數(shù)式編程的特性。在對(duì)函數(shù)式編程的概念有了了解后,本文會(huì)介紹iterators和generators等語(yǔ)言特性,還有itertools和functools等相關(guān)的庫(kù)。
函數(shù)式編程
本章節(jié)將會(huì)介紹函數(shù)式編程的一些基本概念;如果只是對(duì)Python的語(yǔ)言特性感興趣的話,可以跳過。
編程語(yǔ)言支持用幾種不同的方式分解問題。
- 大多數(shù)的編程語(yǔ)言是面向過程的:程序是計(jì)算機(jī)處理輸入的指令集合。C, Pascal, 甚至Unix shell都是這類。
- 在命令式編程中,用戶告訴計(jì)算機(jī)需要做什么,語(yǔ)言的實(shí)現(xiàn)來(lái)完成高效的計(jì)算。SQL可能是最廣為人知的宣告式編程語(yǔ)言了;SQL語(yǔ)句負(fù)責(zé)查詢描述要獲取的數(shù)據(jù)集合,SQL引擎決定是掃描表或者使用索引,首先執(zhí)行哪些字查詢等等問題。
- 面向?qū)ο蟮某绦虿僮鲗?duì)象的集合。對(duì)象具有內(nèi)部狀態(tài),并支持查詢和修改內(nèi)部狀態(tài)的方法。Smalltalk和Java是面向?qū)ο缶幊陶Z(yǔ)言。C++和Python支持面向?qū)ο?,但是不?qiáng)制使用面向?qū)ο筇匦浴?/li>
- 函數(shù)式編程語(yǔ)言將問題分解成一系列函數(shù)。理想情況下,函數(shù)接受輸入產(chǎn)生輸出,并且沒有影響這一過程的內(nèi)部狀態(tài)。眾所周知的函數(shù)式語(yǔ)言包括ML系列(標(biāo)準(zhǔn)ML,OCaml以及其他變種)和Haskell。
設(shè)計(jì)計(jì)算機(jī)語(yǔ)言時(shí),設(shè)計(jì)者會(huì)選擇強(qiáng)調(diào)一種特定的編程方法。這通常會(huì)導(dǎo)致采用另外的方法編寫程序會(huì)變得困難。有一些其他的語(yǔ)言是支持多種不同方法的多范式語(yǔ)言。Lisp,C++和Python是多范式語(yǔ)言;使用這些語(yǔ)言可以編寫面向過程,面向?qū)ο?,或者函?shù)式的程序或者庫(kù)。在一個(gè)大型程序中,可能會(huì)使用不同的方法來(lái)編寫不同的部分。比如,程序中的GUI部分采用面向?qū)ο蠓椒?,而處理的邏輯是面向過程或者是函數(shù)式的。
在一個(gè)函數(shù)式的程序中,輸入會(huì)流經(jīng)一組函數(shù)。每個(gè)函數(shù)對(duì)自己的輸入進(jìn)行處理并產(chǎn)出輸出。對(duì)于那些有修改內(nèi)部狀態(tài)副作用和在進(jìn)行在返回值中不可見的修改的函數(shù),函數(shù)式編程是不鼓勵(lì)的。沒有副作用的函數(shù)被稱之為純函數(shù)。沒有副作用意味著不使用隨程序運(yùn)行而更新的數(shù)據(jù)結(jié)構(gòu);每個(gè)函數(shù)的輸出只取決于它的輸入。
有些編程語(yǔ)言對(duì)于函數(shù)是否是純函數(shù)有著嚴(yán)格的限制,它們甚至沒有類似a=3或者c = a + b這樣的賦值語(yǔ)句,但是想要完全避免副作用是很困難的。比如打印到屏幕或者寫入磁盤文件就是有副作用的。比如在Python中,print和time.sleep(1)都沒有返回有用的值;它們被調(diào)用只是為了發(fā)送文本到屏幕或者暫停執(zhí)行一秒的副作用。
函數(shù)式風(fēng)格的Python程序通常不會(huì)走向避免所有I/O操作或所有賦值操作的極端;它們一般會(huì)提供函數(shù)式的接口,然后內(nèi)部使用非函數(shù)式的特性實(shí)現(xiàn)功能。比如,一個(gè)函數(shù)的內(nèi)部依然會(huì)對(duì)局部變量賦值,但是不會(huì)修改全局變量或者有其他的副作用。
函數(shù)式編程可以看作是面向?qū)ο缶幊痰膶?duì)立面。對(duì)象包含了一些內(nèi)部狀態(tài)和修改這些狀態(tài)的方法,面向?qū)ο蟮某绦蛑付▽?duì)象的正確狀態(tài)。而函數(shù)式編程希望盡量避免狀態(tài)的改變,在函數(shù)之間處理數(shù)據(jù)流。在Python中,你可以通過編程接收和返回對(duì)象來(lái)同時(shí)利用這兩種編程方式,對(duì)象和應(yīng)用有關(guān)(e-mail,事務(wù)等等)。
函數(shù)式的設(shè)計(jì)似乎是一個(gè)奇怪的制約因素。為什么要避免對(duì)象和副作用呢?因?yàn)楹瘮?shù)式風(fēng)格有以下理論和實(shí)踐的優(yōu)勢(shì)。
- 正式證明性
- 模塊化
- 組合性
- 易于調(diào)試和測(cè)試
正式證明性
使用函數(shù)式編程的一個(gè)理論上的好處是,在數(shù)學(xué)上驗(yàn)證一個(gè)程序的正確性是比較容易的。
很長(zhǎng)一段時(shí)間以來(lái),研究人員一直在尋找通過數(shù)學(xué)證明程序正確的方法。這種證明的方法和通過輸入測(cè)試數(shù)據(jù)驗(yàn)證輸出的正確性,以及通過讀取程序的源代碼來(lái)判斷正確性的方法不同;它想要嚴(yán)格證明程序?qū)λ锌赡艿妮斎攵寄墚a(chǎn)生正確的結(jié)果。
用于證明程序正確的技術(shù)是,記下不變量,輸入數(shù)據(jù)和一直為真的程序變量。對(duì)每一行代碼和不變量X,Y,可以知道運(yùn)行前X和Y是否為真,和執(zhí)行后X'和Y'是否為真。進(jìn)行這樣的比較直到程序結(jié)束,這是不變量應(yīng)該符合程序輸出所需的條件。
賦值行為會(huì)修改之前為真的不變量,而不會(huì)產(chǎn)生新的可以向后傳遞的不變量,因此上面的技術(shù)碰到賦值行為的時(shí)候會(huì)難以繼續(xù)下去,這也導(dǎo)致了函數(shù)式風(fēng)格會(huì)避免賦值。
不幸的是,證明程序正確無(wú)疑是不切實(shí)際的,這和Python無(wú)關(guān)。即使是極為簡(jiǎn)單的程序也需要幾頁(yè)長(zhǎng)的證明;中等程度復(fù)雜程序的正確性證明難度將是巨大的,日常使用的程序(Python解釋器,XML解析器,Web瀏覽器)幾乎沒有可以被證明是正確的。即使是生成了一個(gè)證明,也會(huì)存在需要驗(yàn)證這個(gè)證明的問題;如果這個(gè)證明有問題,那么得到程序正確的結(jié)論將是錯(cuò)誤的。
模塊化
函數(shù)式編程一個(gè)更實(shí)際的好處是它會(huì)強(qiáng)制使用者將問題分解。程序因此會(huì)更加模塊化。相比一個(gè)實(shí)現(xiàn)復(fù)雜變換的長(zhǎng)函數(shù),實(shí)現(xiàn)完成一件事的小函數(shù)更加容易。短小的函數(shù)更易于閱讀和檢查錯(cuò)誤。
易于測(cè)試和調(diào)試
函數(shù)式風(fēng)格的程序測(cè)試和調(diào)試起來(lái)更加容易。
函數(shù)通暢很小并且功能明確,所以調(diào)試起來(lái)會(huì)很方便。當(dāng)程序無(wú)法運(yùn)行時(shí),可以在每個(gè)函數(shù)入口檢查數(shù)據(jù)是否正確。可以通過查看中間輸入和輸出,來(lái)快速定位出現(xiàn)bug的函數(shù)。
每個(gè)函數(shù)都可以成為單元測(cè)試的目標(biāo),因此測(cè)試也會(huì)容易些。函數(shù)不依賴于系統(tǒng)狀態(tài),因此測(cè)試只需要合成正確的輸入,然后檢查輸出是否符合預(yù)期。
組合性
在編寫函數(shù)式風(fēng)格的程序時(shí),會(huì)編寫許多具有不同輸入和輸出的函數(shù)。這些函數(shù)有些是專門針對(duì)特定的應(yīng)用,但是其他的函數(shù)將在各種不同的程序中有用。比如,一個(gè)傳入目錄路徑并返回目錄中所有XML文件的函數(shù),或者一個(gè)傳入文件名并返回其內(nèi)容的函數(shù),是可以在多種不同的情況下使用的。
隨著時(shí)間推移,你可以慢慢組建屬于自己的庫(kù)。通常,你可以使用已有的函數(shù),改變配置,然后實(shí)現(xiàn)一些專門針對(duì)當(dāng)前任務(wù)的函數(shù)來(lái)組成新的程序。
Python特性
迭代器
我將首先介紹Python一個(gè)語(yǔ)言特性,這個(gè)特性是編寫函數(shù)式風(fēng)格程序的基礎(chǔ):迭代器。
迭代器是表示數(shù)據(jù)流的對(duì)象;此對(duì)象每次返回?cái)?shù)據(jù)的一個(gè)元素。一個(gè)Python迭代器必須實(shí)現(xiàn)next()方法,該方法不接受參數(shù),并且總是返回?cái)?shù)據(jù)流的下一個(gè)元素。如果流中沒有元素,next()必須拋出StopIteration異常。迭代器不必是有限的;編寫產(chǎn)生無(wú)限數(shù)據(jù)流的迭代器非常合理。
內(nèi)置的iter()函數(shù)接受任意對(duì)象,并嘗試返回一個(gè)返回對(duì)象元素的迭代器,如果對(duì)象不支持迭代,則拋出TypeError異常。Python內(nèi)置的數(shù)據(jù)類型中有幾種是支持迭代的,最常見的有列表和字典。如果一個(gè)對(duì)象可以生成一個(gè)迭代器,則稱該對(duì)象是可迭代的。
可以體驗(yàn)一下迭代接口:
>>> L = [1, 2, 3]
>>> it = iter(L)
>>> print it
<listiterator object at 0x100bc3950>
>>> it.next()
1
>>> it.next()
2
>>> it.next()
3
>>> it.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
在一些不同的上下文中,Python期望對(duì)象是可迭代的,其中最重要的就是for語(yǔ)句。在語(yǔ)句for x in y中,Y必須是迭代器或者能通過iter()方法生成一個(gè)迭代器的對(duì)象。以下兩條語(yǔ)句是等價(jià)的:
for i in iter(obj):
print i
for i in obj:
print i
可以通過list()和tuple()構(gòu)造方法操作迭代器得到列表或者是元組。
>>> L = [1, 2, 3]
>>> iterator = iter(L)
>>> t = tuple(iterator)
>>> t
(1, 2, 3)
序列解包也支持迭代器:如果知道迭代器將返回N個(gè)元素,則可以將其解包為N元組:
>>> L = [1, 2, 3]
>>> iterator = iter(L)
>>> a, b, c = iterator
>>> a, b, c
(1, 2, 3)
內(nèi)置的max()和min()方法接受迭代器作為參數(shù),返回最大或者最小的元素。"in"和"not in"操作同樣支持迭代器:如果X在迭代器返回的流中,則X in iterator返回true。如果迭代器是無(wú)限的話,明顯會(huì)有一些問題;max()和min()將永遠(yuǎn)不會(huì)返回,如果X沒有出現(xiàn)在流中,in和not in也將不會(huì)返回。
請(qǐng)注意,在迭代器中只能向前取數(shù)據(jù),沒有辦法得到上一個(gè)元素,重置迭代器,或者復(fù)制它。迭代器對(duì)象可以可選地提供這些附加功能,但是迭代器協(xié)議僅指定next()方法。因此,函數(shù)可能會(huì)消耗迭代器的所有輸出,如果需要使用相同的流執(zhí)行不同的操作,則必須創(chuàng)建一個(gè)新的迭代器。
支持迭代器的數(shù)據(jù)類型
上面已經(jīng)介紹了列表和元組是如何支持迭代器的。事實(shí)上,任何Python序列類型(如字符串)都支持創(chuàng)建迭代器。
對(duì)一個(gè)字典調(diào)用iter()方法將會(huì)返回一個(gè)迭代器,該迭代器循環(huán)使用字典的鍵。
>>> m = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6, 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
>>> for key in m:
... print key, m[key]
...
Feb 2
Aug 8
Jan 1
Dec 12
Oct 10
Mar 3
Sep 9
May 5
Jun 6
Jul 7
Apr 4
Nov 11
請(qǐng)注意,上面輸出的順序是隨機(jī)的,因?yàn)榕判蚴腔谧值鋵?duì)象的哈希順序。
對(duì)字典對(duì)象使用iter()方法會(huì)返回鍵的迭代器,但是字典有其他方法得到不同的迭代器。如果想迭代鍵,值,鍵/值對(duì),分別可以調(diào)用iterkeys(),itervalues(),iteritems()來(lái)的得到對(duì)應(yīng)的迭代器。
dict()構(gòu)造方法可以接受返回(key, value)元組流的迭代器,生成新的字典。
>>> L = [('Italy', 'Rome'), ('France', 'Paris'), ('US', 'Washington DC')]
>>> dict(iter(L))
{'Italy': 'Rome', 'US': 'Washington DC', 'France': 'Paris'}
文件也可以通過readline()方法來(lái)迭代文件的內(nèi)容,也就是說(shuō)可以通過下面的方式來(lái)讀取文件的每一行:
for line in file:
...
可以從一個(gè)可迭代對(duì)象生成一個(gè)集合,并迭代器中的元素
set(['Italy', 'US', 'France'])
>>> S = set((2, 3, 5, 7, 11, 13))
>>> for i in S:
... print i
...
2
3
5
7
11
13
生成器表達(dá)式和列表推導(dǎo)
對(duì)迭代器通常有兩個(gè)操作:
- 對(duì)每個(gè)元素進(jìn)行操作
- 選擇滿足某個(gè)條件的元素的子集。
比如給定一個(gè)字符串的列表,可能需要去除每個(gè)字符串末尾的空格或者提取包含給定子串的字符串。
列表推導(dǎo)和生成器表達(dá)式是這種操作的簡(jiǎn)寫符號(hào),這是從Haskell中借鑒來(lái)的??梢酝ㄟ^下面的代碼從字符串流中去掉所有空格:
line_list = [' line 1\n', 'line 2 \n', ...]
# 生成器表達(dá)式,返回迭代器
stripped_iter = (line.strip() for line in line_list)
# 列表推導(dǎo)式,返回列表
stripped_list = [line.strip() for line in line_list]
可以通過添加"if"條件來(lái)篩選特定的元素。
stripped_list = [line.strip() for line in line_list if line != ""]
使用列表推導(dǎo)式可以得到一個(gè)Python列表;strip_list是包含所有結(jié)果的列表,不是迭代器。生成器表達(dá)式返回一個(gè)迭代器,根據(jù)需要計(jì)算值,而不需要一次算出所有的值。這意味著,當(dāng)處理一個(gè)無(wú)限的數(shù)據(jù)流或者是一個(gè)數(shù)據(jù)量非常大的迭代器時(shí),列表推導(dǎo)式并不適用。上述的情況應(yīng)該是用生成器表達(dá)式。
生成器表達(dá)式使用()括起來(lái),列表推導(dǎo)式由[]括起來(lái)。生成器表達(dá)式語(yǔ)法如下:
( expression for expr in sequence1
if condition1
for expr2 in sequence2
if condition2
for expr3 in sequence3 ...
if condition3
for exprN in sequenceN
if conditionN )
對(duì)于列表推導(dǎo)式語(yǔ)法,只有外面的括號(hào)不一致。
生成輸出的元素將是expression的連續(xù)值。if子語(yǔ)句都是可選的;只有當(dāng)條件為true時(shí),表達(dá)式才被計(jì)算并添加到結(jié)果中。
生成器表達(dá)式必須寫在括號(hào)內(nèi),也可以寫在表示函數(shù)調(diào)用的括號(hào)內(nèi)。如果將要?jiǎng)?chuàng)建的迭代器馬上會(huì)傳給一個(gè)函數(shù),可以這樣寫:
obj_total = sum(obj.count for obj in list_all_objects())
子語(yǔ)句for...in包含要迭代的序列。哪些序列的長(zhǎng)度不必相同,因?yàn)榈捻樞蚴菑淖蟮接遥皇遣⑿械?。?duì)sequence1中的每個(gè)元素,sequence2都會(huì)從頭迭代。然后對(duì)sequence1和sequence2的每個(gè)結(jié)果對(duì)循環(huán)迭代sequence3。
換句話說(shuō),列表解析或生成器表達(dá)式與以下Python代碼等價(jià):
for expr1 in sequence1:
if not (condition1):
continue # 跳過這個(gè)元素
for expr2 in sequence2:
if not (condition2):
continue # 跳過這個(gè)元素
...
for exprN in sequenceN:
if not (conditionN):
continue # 跳過這個(gè)元素
# 輸出表達(dá)式的值
這意味著,當(dāng)有多個(gè)for...in子語(yǔ)句但沒有if條件的情況下,所得到的輸出的長(zhǎng)度將等于所有序列長(zhǎng)度的乘積。如果有兩個(gè)長(zhǎng)度為3的列表,結(jié)果的長(zhǎng)度就是9。
>>> seq1 = 'abc'
>>> seq2 = (1, 2, 3)
>>> [(x,y) for x in seq1 for y in seq2]
[('a', 1), ('a', 2), ('a', 3), ('b', 1), ('b', 2), ('b', 3), ('c', 1), ('c', 2), ('c', 3)]
為了不引起Python語(yǔ)法上的歧義,如果expression用來(lái)生成一個(gè)元組,則必須將其用()括起來(lái)。下面例子中第一個(gè)有語(yǔ)法錯(cuò)誤,第二個(gè)是正確的。
# 語(yǔ)法錯(cuò)誤
[ x,y for x in seq1 for y in seq2]
# 正確
[ (x,y) for x in seq1 for y in seq2]
生成器
生成器是特殊的一類函數(shù),用于簡(jiǎn)化迭代器的編寫。常規(guī)的函數(shù)計(jì)算一個(gè)值并返回,但是生成器會(huì)返回返回值的迭代器。
你肯定對(duì)Python或者C語(yǔ)言如果調(diào)用函數(shù)很熟悉。當(dāng)一個(gè)函數(shù)被調(diào)用時(shí),它會(huì)創(chuàng)建一個(gè)私有的命名空間,在這個(gè)空間內(nèi)創(chuàng)建局部變量。當(dāng)函數(shù)執(zhí)行到return語(yǔ)句時(shí),局部變量被銷毀,并且將結(jié)果返回給調(diào)用者。該函數(shù)的下一次被調(diào)用時(shí),它會(huì)創(chuàng)建一個(gè)新的私有命名空間和局部變量。但是,如果局部變量在退出時(shí)沒有被銷毀,該怎么辦呢?如果想過一段時(shí)候在上次未執(zhí)行完的地方繼續(xù)執(zhí)行,該怎么辦呢?這就要提到生成器了;它們被認(rèn)為是可恢復(fù)執(zhí)行的函數(shù)。
下面是一個(gè)最簡(jiǎn)單的生成器函數(shù)的例子:
def generate_ints(N):
for i in range(N):
yield i
任何含有yield關(guān)鍵字的函數(shù)都被認(rèn)為是生成器函數(shù);這是由Python字節(jié)碼編譯器檢測(cè)到的,編譯器特別編譯了該函數(shù)。
當(dāng)生成器函數(shù)被調(diào)用時(shí),它不會(huì)返回單個(gè)值,而是會(huì)返回支持迭代器協(xié)議的生成器對(duì)象。在執(zhí)行yield語(yǔ)句時(shí),生成器輸出i的值,這和return語(yǔ)句類似。yield和return語(yǔ)句之間的最大區(qū)別在于,在到達(dá)yield時(shí),生成器的執(zhí)行狀態(tài)將被暫停,并保留局部變量。在下一次調(diào)用生成器的.next()方法時(shí),該函數(shù)將繼續(xù)執(zhí)行。
下面是generate_ints()生成器的使用方法:
>>> gen = generate_ints(3)
>>> gen
<generator object generate_ints at 0x10dfe2a50>
>>> gen.next()
0
>>> gen.next()
1
>>> gen.next()
2
>>> gen.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
寫成for i in generate_ints(3)或者a,b,c=generate_ints(3)也是一樣的。
在生成器函數(shù)中,return語(yǔ)句只能在沒有值的情況下使用,并且表示值的結(jié)束。在執(zhí)行return語(yǔ)句后,生成器不能再返回更多的值。如果在生成器函數(shù)中return帶了返回值,比如return 5,會(huì)被認(rèn)為是語(yǔ)法錯(cuò)誤??梢酝ㄟ^手動(dòng)拋出StopIteration異常,或者讓函數(shù)執(zhí)行到最后,來(lái)讓生成器函數(shù)不再產(chǎn)生新的值。
可以通過自定義的類,將生成器的所有本地變量存儲(chǔ)為實(shí)例,來(lái)達(dá)到生成器的效果。比如,通過將self.count設(shè)置為0,將self.next()實(shí)現(xiàn)為自增self.count并返回,來(lái)返回一列整數(shù)。然而,對(duì)于一個(gè)有一定復(fù)雜度的生成器,實(shí)現(xiàn)自定義的類會(huì)更麻煩。
Python測(cè)試套件中的test_generator.py有更多有意思的例子。下面是一個(gè)遞歸實(shí)現(xiàn)樹的順序遍歷的生成器。
def inorder(t):
if t:
for x in inorder(t.left):
yield x
yield t.label
for x in inorder(t.right):
yield x
另外兩個(gè)例子提供了N皇后問題(在N*N棋盤上放N個(gè)皇后使其彼此不會(huì)互相威脅)和騎士之旅的解法。
向生成器傳值
在Python2.4及之前的版本中,生成器只能產(chǎn)生輸出。一旦生成器的代碼被調(diào)用生成一個(gè)迭代器,當(dāng)函數(shù)恢復(fù)執(zhí)行時(shí),沒有辦法將任何新的信息傳遞到函數(shù)中??梢酝ㄟ^讓生成器檢查全局變量,或者傳遞一些可變對(duì)象供調(diào)用方修改,來(lái)將新信息傳入,但是這樣很不優(yōu)雅。
在Python2.5中,有一個(gè)簡(jiǎn)單的方法將值傳遞給生成器。yield成為了表達(dá)式,返回一個(gè)可以分配給變量或者以其他方式運(yùn)行的值。
val = (yield i)
我推薦大家在執(zhí)行和返回值相關(guān)的操作時(shí),始終將yield表達(dá)式括起來(lái),如上面的例子。這個(gè)括號(hào)并不總是必須的,但是總是添加它比記住何時(shí)需要它們更容易。
通過調(diào)用send(value)方法,可以將值傳到生成器中。這個(gè)方法繼續(xù)執(zhí)行生成器的代碼,yield表達(dá)式會(huì)返回傳入的值。如果next()方法被調(diào)用,yield返回None。
下面是一個(gè)每次增加1的簡(jiǎn)單計(jì)數(shù)器,允許修改內(nèi)部計(jì)數(shù)器的值。
def counter(maximum):
i = 0
while i < maximum:
val = (yield i)
# 如果給定value,改變計(jì)數(shù)器
if val is not None:
i = val
else:
i += 1
下面是改變計(jì)數(shù)器的方法:
>>> it = counter(10)
>>> print it.next()
0
>>> print it.next()
1
>>> it.send(8)
8
>>> print it.next()
9
>>> print it.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
因?yàn)?code>yield會(huì)經(jīng)常返回None,代碼中應(yīng)該檢查這種情況。不要在表達(dá)式中使用這個(gè)值,除非確定send()是恢復(fù)函數(shù)的唯一方法。
除了send(),生成器還有兩個(gè)新的方法:
-
throw(type, value=None, traceback=None)用于在生成器中拋出異常;當(dāng)生成器暫停執(zhí)行時(shí),由yield語(yǔ)句拋出這個(gè)異常。 -
close()通過在生成器中拋出GeneratorExit異常來(lái)終止迭代。在拋出這個(gè)異常后,生成器代碼必須拋出GeneratorExit或者StopIteration;捕獲這個(gè)異常是非法的,會(huì)觸發(fā)RuntimeError。在Python的垃圾回收器對(duì)生成器進(jìn)行回收時(shí)也會(huì)調(diào)用close()。
處理GeneratorExit異常推薦使用try: ... finally:而不是catch GeneratorExit。
上面的改動(dòng)讓生成器由單向生產(chǎn)者變?yōu)樯a(chǎn)者和消費(fèi)者。
生成器也成為了協(xié)程--一種更通用的子程序。子程序在一個(gè)點(diǎn)進(jìn)入,在另外點(diǎn)退出(函數(shù)的入口和return語(yǔ)句),但是協(xié)程可以在多個(gè)不同的點(diǎn)進(jìn)入,退出,恢復(fù)執(zhí)行(yield語(yǔ)句)。
內(nèi)置函數(shù)
現(xiàn)在來(lái)看看迭代器常用的內(nèi)置函數(shù)。
兩個(gè)Python內(nèi)置的函數(shù)map()和filter()在某種程度上已經(jīng)被淘汰了;它們的功能和列表推導(dǎo)式重合,不過返回的是實(shí)際的列表,而不是迭代器。
map(f, iterA, iterB, ...)返回一個(gè)列表,內(nèi)容是f(iterA[0], iterB[0]), f(iterA[1], iterB[1]), f(iterA[2], iterB[2]), ...。
>>> def upper(s):
... return s.upper()
>>> map(upper, ['sentence', 'fragment'])
['SENTENCE', 'FRAGMENT']
>>> [upper(s) for s in ['sentence', 'fragment']]
['SENTENCE', 'FRAGMENT']
正如上面所展示的,使用列表推導(dǎo)式可以實(shí)現(xiàn)一樣的效果。itertools.imap()完成一樣的功能,不過它可以處理無(wú)盡的迭代器;等會(huì)在討論itertools模塊的再談。
filter(predicate, iter)返回一個(gè)包含滿足特定條件的所有序列元素的列表,這個(gè)功能和列表推導(dǎo)重復(fù)。predicate是返回一些條件的真值的函數(shù);為了和filter()一起使用,predicate只能傳入一個(gè)參數(shù)。
>>> def is_even(x):
... return (x % 2) == 0
>>> filter(is_even, range(10))
[0, 2, 4, 6, 8]
上面的功能也可以使用列表推導(dǎo)式完成:
>>> [x for x in range(10) if is_even(x)]
[0, 2, 4, 6, 8]
filter()在itertools模塊中也有對(duì)應(yīng)的方法,itertools.ifilter,這個(gè)方法返回一個(gè)迭代器,因此也可以像itertools.imap()一樣處理無(wú)限序列。
reduce(func, iter, [initial_value])在itertools模塊中沒有對(duì)應(yīng)的方法,因?yàn)樗鄯e地對(duì)可迭代對(duì)象的所有元素執(zhí)行操作,因此不能用于無(wú)限迭代。func函數(shù)必須接受兩個(gè)參數(shù)并返回一個(gè)值。reduce()接受迭代器返回的前兩個(gè)元素A和B,返回func(A, B)。之后請(qǐng)求第三個(gè)元素C,計(jì)算func(func(A, B), C),然后請(qǐng)求迭代器返回的第四個(gè)元素,持續(xù)這樣的步驟一直到迭代完所有元素。如果可迭代對(duì)象不返回任何元素,會(huì)拋出TypeError異常。如果提供了初始值,第一輪運(yùn)算會(huì)是func(initial_value, A)。
>>> import operator
>>> reduce(operator.concat, ['A', 'BB', 'C'])
'ABBC'
>>> reduce(operator.concat, [])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: reduce() of empty sequence with no initial value
>>> reduce(operator.mul, [1,2,3], 1)
6
>>> reduce(operator.mul, [], 1)
1
如果在reduce()中使用operator.add(),會(huì)得到迭代對(duì)象所有值的和。這個(gè)場(chǎng)景使用非常廣泛,有內(nèi)置的sum()方法來(lái)計(jì)算。
>>> reduce(operator.add, [1,2,3,4], 0)
10
>>> sum([1,2,3,4])
10
>>> sum([])
0
在很多reduce()的使用場(chǎng)景中,使用for循環(huán)會(huì)更好。
product = reduce(operator.mul, [1,2,3], 1)
# 等同于
product = 1
for i in [1,2,3]:
product *= i
enumerate(iter)對(duì)可迭代對(duì)象中的元素進(jìn)行計(jì)數(shù),返回計(jì)數(shù)值和沒有元素的二元組。
>>> for item in enumerate(['subject', 'verb', 'object']):
... print item
...
(0, 'subject')
(1, 'verb')
(2, 'object')
循環(huán)遍歷列表并記錄滿足某些條件的索引時(shí),常常使用enumerate():
f = open('data.txt', 'r')
for i, line in enumerate(f):
if line.strip() == '':
print 'Blank line at line #%i' % i
sorted(iterable, [cmp=None], [key=None], [reverse=False])將可迭代對(duì)象中的所有元素收集到列表中,對(duì)列表進(jìn)行排序,并返回排序結(jié)果。cmp,key和reverse參數(shù)傳遞給構(gòu)造的列表的.sort()方法。
>>> import random
>>> rand_list = random.sample(range(10000), 8)
>>> rand_list
[3027, 8533, 16, 6602, 4183, 9577, 4842, 5713]
>>> sorted(rand_list)
[16, 3027, 4183, 4842, 5713, 6602, 8533, 9577]
>>> sorted(rand_list, reverse=True)
[9577, 8533, 6602, 5713, 4842, 4183, 3027, 16]
內(nèi)置的any(iter)和all(iter)函數(shù)查看一個(gè)可迭代對(duì)象的真值。如果有任意元素為真值,any()返回True,如果所有元素都為真值,all()返回True。
>>> any([0,1,0])
True
>>> any([0,0,0])
False
>>> any([1,1,1])
True
>>> all([0,1,0])
False
>>> all([0,0,0])
False
>>> all([1,1,1])
True
小函數(shù)和lambda表達(dá)式
編寫函數(shù)式程序時(shí),通常需要很少的功能作為謂詞或以某種方式組合元素。
如果有一個(gè)Python內(nèi)置函數(shù)或是模塊是合適的,那么就不需要重新定義一個(gè)新的功能:
stripped_lines = [line.strip() for line in lines]
existing_lines = filter(os.path.exists, file_list)
如果不存在現(xiàn)有的函數(shù),那么你需要實(shí)現(xiàn)一個(gè)。編寫小函數(shù)的一種方法是使用lambda語(yǔ)句。lambda需要一些參數(shù)和處理這些參數(shù)的表達(dá)式,并創(chuàng)建一個(gè)返回表達(dá)式值的小函數(shù):
lowercase = lambda x: x.lower()
print_assign = lambda name, value: name + '=' + str(value)
adder = lambda x, y: x+y
一種替代的方法是用def定義一個(gè)函數(shù):
def lowercase(x):
return x.lower()
def print_assign(name, value):
return name + '=' + str(value)
def adder(x, y):
return x + y
哪種方式更好呢?這是一個(gè)編程風(fēng)格的問題;我通常的做法是避免使用lambda。
原因是lambda在可以定義的功能上是非常有限的。結(jié)果必須作為單個(gè)表達(dá)式計(jì)算,這意味著不能使用if... elif... else和try... except語(yǔ)句。如果試圖在lambda語(yǔ)句中做太多的事情,那么最終會(huì)出現(xiàn)一個(gè)難以理解的過于復(fù)雜的表達(dá)式。能快速說(shuō)出下面代碼的作用嗎?
total = reduce(lambda a, b: (0, a[1]+b[1]), items)[1]
需要一些時(shí)間來(lái)弄清楚表達(dá)方式,才能弄清楚代碼想要干什么。使用一個(gè)簡(jiǎn)短的嵌套的def語(yǔ)句會(huì)好一些:
def combine(a, b):
return 0, a[1] + b[1]
total = reduce(combine, items)[1]
如果只用一個(gè)for循環(huán)就更好了。
total = 0
for a, b in items:
total += b
或者使用內(nèi)置的sum()和生成器表達(dá)式
total = sum(b for a, b in items)
很多時(shí)候使用for循環(huán)比使用reduce()代碼更加清晰。
Fredrik Lundh曾經(jīng)提出過以下lambda重構(gòu)的規(guī)則:
- 寫一個(gè)lambda函數(shù)。
- 寫一個(gè)注釋,說(shuō)明該lambda函數(shù)的作用。
- 研究注釋一段時(shí)間,并想出一個(gè)名字來(lái)捕捉評(píng)論的本職。
- 使用該名稱將lambda函數(shù)轉(zhuǎn)換為def語(yǔ)句。
- 移除注釋。
我很喜歡這些規(guī)則,但是你可以有自己的選擇。
itertools模塊
itertools模塊包含許多常用的迭代器,以及用于組合幾個(gè)迭代器的函數(shù)。本節(jié)將通過幾個(gè)小例子來(lái)介紹模塊的內(nèi)容。
該模塊的功能分為幾類:
- 基于現(xiàn)有迭代器創(chuàng)建新迭代器的函數(shù)。
- 將迭代器作為函數(shù)的參數(shù)。
- 用于選擇迭代器部分輸出的函數(shù)。
- 對(duì)迭代器輸出進(jìn)行分組的函數(shù)。
創(chuàng)建新的迭代器
函數(shù)itertools.count(n)返回?zé)o限的整數(shù)流,每次增加1??梢赃x擇起始的數(shù)字,默認(rèn)是0:
itertools.count() =>
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
itertools.count(10) =>
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...
函數(shù)itertools.cycle(iter)保存可迭代對(duì)象內(nèi)容的副本,返回一個(gè)新的迭代器,該迭代器從頭到尾返回其元素。新的迭代器將無(wú)限重復(fù)這些元素。
itertools.cycle([1,2,3,4,5]) =>
1, 2, 3, 4, 5, 1, 2, 3, 4, 5, ...
itertools.repeat(elem, [n])對(duì)傳入的元素重復(fù)n次,如果n沒有提供,則無(wú)限返回該元素。
itertools.repeat('abc') =>
abc, abc, abc, abc, abc, abc, abc, abc, abc, ...
itertools.repeat('abc', 5) =>
abc, abc, abc, abc, abc
itertools.chain(iterA, iterB, ...)輸入任意數(shù)量的可迭代對(duì)象,返回第一個(gè)迭代器的所有元素,然后返回第二個(gè)迭代器的所有元素,依次類推,知道返回最有一個(gè)迭代器的所有元素。
itertools.chain(['a', 'b', 'c'], (1, 2, 3)) =>
a, b, c, 1, 2, 3
itertools.izip(iterA, iterB, ...)每次從可迭代對(duì)象中讀取一個(gè)元素,然后在元組中返回它們:
itertools.izip(['a', 'b', 'c'], (1, 2, 3)) =>
('a', 1), ('b', 2), ('c', 3)
這個(gè)和內(nèi)置的zip()函數(shù)類似,但是不構(gòu)成內(nèi)存列表,并在返回之前耗盡所有的輸入迭代器;只有當(dāng)它們被請(qǐng)求時(shí),才構(gòu)造并返回元組。(專業(yè)術(shù)語(yǔ)叫做惰性求值)。
該迭代器旨在于所有長(zhǎng)度相同的可迭代對(duì)象使用。如果可迭代對(duì)象長(zhǎng)度不同,生成的流將與最短的可迭代對(duì)象長(zhǎng)度相同。
itertools.izip(['a', 'b'], (1, 2, 3)) =>
('a', 1), ('b', 2)
但是應(yīng)該避免這樣做,因?yàn)閺母L(zhǎng)的可迭代對(duì)象中讀取的元素可能會(huì)被丟棄。這意味著不能進(jìn)一步使用該可迭代對(duì)象,因?yàn)橛刑^丟棄元素的風(fēng)險(xiǎn)。
itertools.islice(iter, [start], stop, [step])返回迭代器的切片流。它會(huì)在遇到第一個(gè)stop的元素時(shí)返回。如果提供了起始索引,將會(huì)獲得stop-start之間的元素,如果提供了step值,將會(huì)相應(yīng)地跳過元素。和Python字符串和列表的切片不同,這里的start,stop,step不能取負(fù)值。
itertools.islice(range(10), 8) =>
0, 1, 2, 3, 4, 5, 6, 7
itertools.islice(range(10), 2, 8) =>
2, 3, 4, 5, 6, 7
itertools.islice(range(10), 2, 8, 2) =>
2, 4, 6
itertools.tee(iter, [n])復(fù)制一個(gè)迭代器;它返回n個(gè)獨(dú)立的迭代器,它們將返回源迭代器的內(nèi)容。n的默認(rèn)值是2。復(fù)制迭代器需要保存源迭代器的一些內(nèi)容,因此如果源迭代器很大并且一個(gè)新的迭代器比其他迭代器更多被消費(fèi),那么這會(huì)消耗很可觀的內(nèi)存。
itertools.tee(itertools.count()) =>
iterA, iterB
iterA ->
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
iterB ->
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
對(duì)可迭代對(duì)象的元素進(jìn)行操作
有兩個(gè)函數(shù)用于對(duì)可迭代的內(nèi)容進(jìn)行其他函數(shù)的調(diào)用。
itertools.imap(f, iterA, iterB)返回包含f(iterA[0], iterB[0]), f(iterA[1], iterB[1]), f(iterA[2], iterB[2]), ...的流:
itertools.imap(operator.add, [5, 6, 5], [1, 2, 3]) =>
6, 8, 8
operator模塊包含一組與Python運(yùn)算符相對(duì)應(yīng)的函數(shù)。比如operator.add(a, b)(兩個(gè)元素相加),operator.ne(a, b)(等同于a!=b),和operator.attrgetter('id')(返回一個(gè)可以獲取"id"屬性的可調(diào)用方法)。
itertools.starmap(func, iter)假定可迭代對(duì)象返回一個(gè)元組流,并使用這些元組作為參數(shù)調(diào)用f():
itertools.starmap(os.path.join,
[('/usr', 'bin', 'java'), ('/bin', 'python'),
('/usr', 'bin', 'perl'), ('/usr', 'bin', 'ruby')])
=>
/usr/bin/java, /bin/python, /usr/bin/perl, /usr/bin/ruby
選擇元素
另一組函數(shù)根據(jù)謂詞選擇迭代器元素的子集。
itertools.ifilter(predicate, iter)返回所有predicate為真的元素:
def is_even(x):
return (x % 2) == 0
itertools.ifilter(is_even, itertools.count()) =>
0, 2, 4, 6, 8, 10, 12, 14, ..
itertools.ifilterfalse(predicate, iter)正好相反,返回所有為假的元素:
itertools.ifilterfalse(is_even, itertools.count()) =>
1, 3, 5, 7, 9, 11, 13, 15, ...
itertools.takewhile(predicate, iter)只要predicate為真,就一直返回值。一旦predicate為假,就停止迭代。
def less_than_10(x):
return (x < 10)
itertools.takewhile(less_than_10, itertoosl.count()) =>
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
itertools.takewhile(is_even, itertools.count()) =>
0
itertools.dropwhile(predicate, iter)丟棄所有predicate為真的值,返回其他值。
itertools.dropwhile(less_than_10, itertools.count()) =>
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...
itertools.dropwhile(is_even, itertools.count()) =>
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...
元素分組
現(xiàn)在談的最后一個(gè)函數(shù)itertools.groupby(iter, key_func=None)是最復(fù)雜的。key_func(elem)是一個(gè)可以計(jì)算可迭代對(duì)象返回的每個(gè)元素鍵值的函數(shù)。如果沒有傳入這個(gè)函數(shù),鍵值就是元素本身。
函數(shù)groupby()從具有相同鍵值的底層迭代中收集所有連續(xù)元素,并返回一個(gè)包含鍵值的2元組流河具有該鍵的元素的迭代器。
city_list = [('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL'),
('Anchorage', 'AK'), ('Nome', 'AK'),
('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ'),
...
]
def get_state((city, state)):
return state
itertools.groupby(city_list, get_state) =>
('AL', iterator-1),
('AK', iterator-2),
('AZ', iterator-3), ...
iterator-1 =>
('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL')
iterator-2 =>
('Anchorage', 'AK'), ('Nome', 'AK')
iterator-3 =>
('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ')
函數(shù)groupby()假定底層可迭代對(duì)象的內(nèi)容已經(jīng)根據(jù)鍵值進(jìn)行排序。請(qǐng)注意,返回的迭代器依然使用底層的可迭代對(duì)象,因此必須在消費(fèi)iterator-1中的內(nèi)容后,再去請(qǐng)求iterator-2及其相應(yīng)的鍵值。
functools模塊
在Python2.5中引入的functools模塊包含一些高階函數(shù)。高階函數(shù)將一個(gè)或多個(gè)函數(shù)作為輸入,并返回一個(gè)新的函數(shù)。其中最有用的就是functools.partial()函數(shù)。
對(duì)函數(shù)式風(fēng)格的程序來(lái)說(shuō),有時(shí)要構(gòu)建具有填充一些參數(shù)的現(xiàn)有函數(shù)的變體。比如函數(shù)f(a, b, c);你可能希望創(chuàng)建一個(gè)新的函數(shù)g(b, c),相當(dāng)于f(1, b, c);這被稱之為“部分功能應(yīng)用程序”。
partial的構(gòu)造函數(shù)使用參數(shù)(function, arg1, arg2, ... kwarg1=value1, kwarg2=value2)。生成的對(duì)象是可調(diào)用的,所以可以用它來(lái)構(gòu)造可以填充參數(shù)的函數(shù)。
下面是一個(gè)小例子:
import functools
def log(message, subsystem):
# 向特定的system寫入消息
print '%s: %s' % (subsystem, message)
...
server_log = functools.partial(log, subsystem='server')
server_log('Unable to open socket')
operator模塊
之前提到過operator模塊。它包含一組于Python操作符相對(duì)應(yīng)的函數(shù)。這些函數(shù)在函數(shù)式風(fēng)格的代碼中通常很有用,因?yàn)樗梢蕴娲恍┲话▎蝹€(gè)操作的函數(shù)。
其中的一些函數(shù)有:
- 數(shù)學(xué)運(yùn)算:
add(),sub(),mul(),div(),floordiv(),abs(),... - 邏輯運(yùn)算:
not_(),truth()。 - 位運(yùn)算:
and_(),or_(),invert()。 - 對(duì)比:
eq(),ne(),lt(),le(),gt()和ge()。 - 對(duì)象標(biāo)識(shí):
is_(),is_not()。
請(qǐng)參閱operator模塊的文檔來(lái)獲取完整列表。