Python函數(shù)式編程

本文翻譯自Functional Programming Howto

lambda

本文將介紹Python中函數(shù)式編程的特性。在對(duì)函數(shù)式編程的概念有了了解后,本文會(huì)介紹iterators和generators等語(yǔ)言特性,還有itertoolsfunctools等相關(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中,printtime.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)在流中,innot 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è)操作:

  1. 對(duì)每個(gè)元素進(jìn)行操作
  2. 選擇滿足某個(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ì)sequence1sequence2的每個(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ǔ)句類似。yieldreturn語(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,keyreverse參數(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... elsetry... 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ī)則:

  1. 寫一個(gè)lambda函數(shù)。
  2. 寫一個(gè)注釋,說(shuō)明該lambda函數(shù)的作用。
  3. 研究注釋一段時(shí)間,并想出一個(gè)名字來(lái)捕捉評(píng)論的本職。
  4. 使用該名稱將lambda函數(shù)轉(zhuǎn)換為def語(yǔ)句。
  5. 移除注釋。

我很喜歡這些規(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)獲取完整列表。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容