Python函數(shù)式編程

本文翻譯自Functional Programming Howto

lambda

本文將介紹Python中函數(shù)式編程的特性不撑。在對函數(shù)式編程的概念有了了解后,本文會介紹iterators和generators等語言特性,還有itertoolsfunctools等相關(guān)的庫宣渗。

函數(shù)式編程

本章節(jié)將會介紹函數(shù)式編程的一些基本概念嘶是;如果只是對Python的語言特性感興趣的話,可以跳過。

編程語言支持用幾種不同的方式分解問題究反。

  • 大多數(shù)的編程語言是面向過程的:程序是計算機(jī)處理輸入的指令集合寻定。C, Pascal, 甚至Unix shell都是這類。
  • 在命令式編程中精耐,用戶告訴計算機(jī)需要做什么狼速,語言的實(shí)現(xiàn)來完成高效的計算。SQL可能是最廣為人知的宣告式編程語言了卦停;SQL語句負(fù)責(zé)查詢描述要獲取的數(shù)據(jù)集合向胡,SQL引擎決定是掃描表或者使用索引,首先執(zhí)行哪些字查詢等等問題惊完。
  • 面向?qū)ο蟮某绦虿僮鲗ο蟮募辖┣邸ο缶哂袃?nèi)部狀態(tài),并支持查詢和修改內(nèi)部狀態(tài)的方法专执。Smalltalk和Java是面向?qū)ο缶幊陶Z言淮捆。C++和Python支持面向?qū)ο螅遣粡?qiáng)制使用面向?qū)ο筇匦浴?/li>
  • 函數(shù)式編程語言將問題分解成一系列函數(shù)本股。理想情況下攀痊,函數(shù)接受輸入產(chǎn)生輸出,并且沒有影響這一過程的內(nèi)部狀態(tài)拄显。眾所周知的函數(shù)式語言包括ML系列(標(biāo)準(zhǔn)ML苟径,OCaml以及其他變種)和Haskell。

設(shè)計計算機(jī)語言時躬审,設(shè)計者會選擇強(qiáng)調(diào)一種特定的編程方法棘街。這通常會導(dǎo)致采用另外的方法編寫程序會變得困難。有一些其他的語言是支持多種不同方法的多范式語言承边。Lisp遭殉,C++和Python是多范式語言;使用這些語言可以編寫面向過程博助,面向?qū)ο笙瘴郏蛘吆瘮?shù)式的程序或者庫。在一個大型程序中富岳,可能會使用不同的方法來編寫不同的部分蛔糯。比如,程序中的GUI部分采用面向?qū)ο蠓椒ń咽剑幚淼倪壿嬍敲嫦蜻^程或者是函數(shù)式的蚁飒。

在一個函數(shù)式的程序中,輸入會流經(jīng)一組函數(shù)萝喘。每個函數(shù)對自己的輸入進(jìn)行處理并產(chǎn)出輸出淮逻。對于那些有修改內(nèi)部狀態(tài)副作用和在進(jìn)行在返回值中不可見的修改的函數(shù)琼懊,函數(shù)式編程是不鼓勵的。沒有副作用的函數(shù)被稱之為純函數(shù)爬早。沒有副作用意味著不使用隨程序運(yùn)行而更新的數(shù)據(jù)結(jié)構(gòu)肩碟;每個函數(shù)的輸出只取決于它的輸入。

有些編程語言對于函數(shù)是否是純函數(shù)有著嚴(yán)格的限制凸椿,它們甚至沒有類似a=3或者c = a + b這樣的賦值語句,但是想要完全避免副作用是很困難的翅溺。比如打印到屏幕或者寫入磁盤文件就是有副作用的脑漫。比如在Python中,printtime.sleep(1)都沒有返回有用的值咙崎;它們被調(diào)用只是為了發(fā)送文本到屏幕或者暫停執(zhí)行一秒的副作用优幸。

函數(shù)式風(fēng)格的Python程序通常不會走向避免所有I/O操作或所有賦值操作的極端;它們一般會提供函數(shù)式的接口褪猛,然后內(nèi)部使用非函數(shù)式的特性實(shí)現(xiàn)功能网杆。比如,一個函數(shù)的內(nèi)部依然會對局部變量賦值伊滋,但是不會修改全局變量或者有其他的副作用碳却。

函數(shù)式編程可以看作是面向?qū)ο缶幊痰膶α⒚妗ο蟀艘恍﹥?nèi)部狀態(tài)和修改這些狀態(tài)的方法笑旺,面向?qū)ο蟮某绦蛑付▽ο蟮恼_狀態(tài)昼浦。而函數(shù)式編程希望盡量避免狀態(tài)的改變,在函數(shù)之間處理數(shù)據(jù)流筒主。在Python中关噪,你可以通過編程接收和返回對象來同時利用這兩種編程方式,對象和應(yīng)用有關(guān)(e-mail乌妙,事務(wù)等等)使兔。

函數(shù)式的設(shè)計似乎是一個奇怪的制約因素。為什么要避免對象和副作用呢藤韵?因?yàn)楹瘮?shù)式風(fēng)格有以下理論和實(shí)踐的優(yōu)勢虐沥。

  • 正式證明性
  • 模塊化
  • 組合性
  • 易于調(diào)試和測試

正式證明性

使用函數(shù)式編程的一個理論上的好處是,在數(shù)學(xué)上驗(yàn)證一個程序的正確性是比較容易的荠察。

很長一段時間以來置蜀,研究人員一直在尋找通過數(shù)學(xué)證明程序正確的方法。這種證明的方法和通過輸入測試數(shù)據(jù)驗(yàn)證輸出的正確性悉盆,以及通過讀取程序的源代碼來判斷正確性的方法不同盯荤;它想要嚴(yán)格證明程序?qū)λ锌赡艿妮斎攵寄墚a(chǎn)生正確的結(jié)果。

用于證明程序正確的技術(shù)是焕盟,記下不變量秋秤,輸入數(shù)據(jù)和一直為真的程序變量宏粤。對每一行代碼和不變量X,Y灼卢,可以知道運(yùn)行前X和Y是否為真绍哎,和執(zhí)行后X'和Y'是否為真。進(jìn)行這樣的比較直到程序結(jié)束鞋真,這是不變量應(yīng)該符合程序輸出所需的條件崇堰。

賦值行為會修改之前為真的不變量,而不會產(chǎn)生新的可以向后傳遞的不變量涩咖,因此上面的技術(shù)碰到賦值行為的時候會難以繼續(xù)下去海诲,這也導(dǎo)致了函數(shù)式風(fēng)格會避免賦值。

不幸的是檩互,證明程序正確無疑是不切實(shí)際的特幔,這和Python無關(guān)。即使是極為簡單的程序也需要幾頁長的證明;中等程度復(fù)雜程序的正確性證明難度將是巨大的闸昨,日常使用的程序(Python解釋器蚯斯,XML解析器,Web瀏覽器)幾乎沒有可以被證明是正確的饵较。即使是生成了一個證明拍嵌,也會存在需要驗(yàn)證這個證明的問題;如果這個證明有問題循诉,那么得到程序正確的結(jié)論將是錯誤的撰茎。

模塊化

函數(shù)式編程一個更實(shí)際的好處是它會強(qiáng)制使用者將問題分解。程序因此會更加模塊化打洼。相比一個實(shí)現(xiàn)復(fù)雜變換的長函數(shù)龄糊,實(shí)現(xiàn)完成一件事的小函數(shù)更加容易。短小的函數(shù)更易于閱讀和檢查錯誤募疮。

易于測試和調(diào)試

函數(shù)式風(fēng)格的程序測試和調(diào)試起來更加容易载碌。

函數(shù)通暢很小并且功能明確花椭,所以調(diào)試起來會很方便详恼。當(dāng)程序無法運(yùn)行時默刚,可以在每個函數(shù)入口檢查數(shù)據(jù)是否正確“疟校可以通過查看中間輸入和輸出筋蓖,來快速定位出現(xiàn)bug的函數(shù)。

每個函數(shù)都可以成為單元測試的目標(biāo)退敦,因此測試也會容易些粘咖。函數(shù)不依賴于系統(tǒng)狀態(tài),因此測試只需要合成正確的輸入侈百,然后檢查輸出是否符合預(yù)期瓮下。

組合性

在編寫函數(shù)式風(fēng)格的程序時翰铡,會編寫許多具有不同輸入和輸出的函數(shù)。這些函數(shù)有些是專門針對特定的應(yīng)用讽坏,但是其他的函數(shù)將在各種不同的程序中有用锭魔。比如,一個傳入目錄路徑并返回目錄中所有XML文件的函數(shù)路呜,或者一個傳入文件名并返回其內(nèi)容的函數(shù)迷捧,是可以在多種不同的情況下使用的。

隨著時間推移胀葱,你可以慢慢組建屬于自己的庫党涕。通常,你可以使用已有的函數(shù)巡社,改變配置,然后實(shí)現(xiàn)一些專門針對當(dāng)前任務(wù)的函數(shù)來組成新的程序手趣。

Python特性

迭代器

我將首先介紹Python一個語言特性晌该,這個特性是編寫函數(shù)式風(fēng)格程序的基礎(chǔ):迭代器。

迭代器是表示數(shù)據(jù)流的對象绿渣;此對象每次返回數(shù)據(jù)的一個元素朝群。一個Python迭代器必須實(shí)現(xiàn)next()方法,該方法不接受參數(shù)中符,并且總是返回數(shù)據(jù)流的下一個元素姜胖。如果流中沒有元素,next()必須拋出StopIteration異常淀散。迭代器不必是有限的右莱;編寫產(chǎn)生無限數(shù)據(jù)流的迭代器非常合理。

內(nèi)置的iter()函數(shù)接受任意對象档插,并嘗試返回一個返回對象元素的迭代器慢蜓,如果對象不支持迭代,則拋出TypeError異常郭膛。Python內(nèi)置的數(shù)據(jù)類型中有幾種是支持迭代的晨抡,最常見的有列表和字典。如果一個對象可以生成一個迭代器则剃,則稱該對象是可迭代的耘柱。

可以體驗(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期望對象是可迭代的棍现,其中最重要的就是for語句调煎。在語句for x in y中,Y必須是迭代器或者能通過iter()方法生成一個迭代器的對象己肮。以下兩條語句是等價的:

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個元素烈涮,則可以將其解包為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。如果迭代器是無限的話西土,明顯會有一些問題讶舰;max()min()將永遠(yuǎn)不會返回,如果X沒有出現(xiàn)在流中需了,innot in也將不會返回跳昼。

請注意,在迭代器中只能向前取數(shù)據(jù)肋乍,沒有辦法得到上一個元素鹅颊,重置迭代器,或者復(fù)制它墓造。迭代器對象可以可選地提供這些附加功能堪伍,但是迭代器協(xié)議僅指定next()方法。因此觅闽,函數(shù)可能會消耗迭代器的所有輸出帝雇,如果需要使用相同的流執(zhí)行不同的操作,則必須創(chuàng)建一個新的迭代器蛉拙。

支持迭代器的數(shù)據(jù)類型

上面已經(jīng)介紹了列表和元組是如何支持迭代器的尸闸。事實(shí)上,任何Python序列類型(如字符串)都支持創(chuàng)建迭代器孕锄。

對一個字典調(diào)用iter()方法將會返回一個迭代器吮廉,該迭代器循環(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

請注意畸肆,上面輸出的順序是隨機(jī)的茧痕,因?yàn)榕判蚴腔谧值鋵ο蟮墓m樞颉?/p>

對字典對象使用iter()方法會返回鍵的迭代器,但是字典有其他方法得到不同的迭代器恼除。如果想迭代鍵踪旷,值,鍵/值對豁辉,分別可以調(diào)用iterkeys(),itervalues(),iteritems()來的得到對應(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()方法來迭代文件的內(nèi)容气破,也就是說可以通過下面的方式來讀取文件的每一行:

for line in file:
    ...

可以從一個可迭代對象生成一個集合,并迭代器中的元素

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)

對迭代器通常有兩個操作:

  1. 對每個元素進(jìn)行操作
  2. 選擇滿足某個條件的元素的子集餐抢。
    比如給定一個字符串的列表现使,可能需要去除每個字符串末尾的空格或者提取包含給定子串的字符串低匙。

列表推導(dǎo)和生成器表達(dá)式是這種操作的簡寫符號,這是從Haskell中借鑒來的碳锈⊥缫保可以通過下面的代碼從字符串流中去掉所有空格:

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"條件來篩選特定的元素强重。

stripped_list = [line.strip() for line in line_list if line != ""]

使用列表推導(dǎo)式可以得到一個Python列表;strip_list是包含所有結(jié)果的列表贸人,不是迭代器间景。生成器表達(dá)式返回一個迭代器,根據(jù)需要計算值艺智,而不需要一次算出所有的值倘要。這意味著,當(dāng)處理一個無限的數(shù)據(jù)流或者是一個數(shù)據(jù)量非常大的迭代器時十拣,列表推導(dǎo)式并不適用封拧。上述的情況應(yīng)該是用生成器表達(dá)式。

生成器表達(dá)式使用()括起來父晶,列表推導(dǎo)式由[]括起來。生成器表達(dá)式語法如下:

( 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 )

對于列表推導(dǎo)式語法弄跌,只有外面的括號不一致甲喝。

生成輸出的元素將是expression的連續(xù)值。if子語句都是可選的铛只;只有當(dāng)條件為true時埠胖,表達(dá)式才被計算并添加到結(jié)果中。

生成器表達(dá)式必須寫在括號內(nèi)淳玩,也可以寫在表示函數(shù)調(diào)用的括號內(nèi)直撤。如果將要創(chuàng)建的迭代器馬上會傳給一個函數(shù),可以這樣寫:

obj_total = sum(obj.count for obj in list_all_objects())

子語句for...in包含要迭代的序列蜕着。哪些序列的長度不必相同谋竖,因?yàn)榈捻樞蚴菑淖蟮接遥皇遣⑿械某邢弧?code>sequence1中的每個元素蓖乘,sequence2都會從頭迭代。然后對sequence1sequence2的每個結(jié)果對循環(huán)迭代sequence3韧骗。

換句話說嘉抒,列表解析或生成器表達(dá)式與以下Python代碼等價:

for expr1 in sequence1:
    if not (condition1):
        continue   # 跳過這個元素
    for expr2 in sequence2:
        if not (condition2):
            continue   # 跳過這個元素
        ...
        for exprN in sequenceN:
            if not (conditionN):
                continue   # 跳過這個元素

            # 輸出表達(dá)式的值

這意味著,當(dāng)有多個for...in子語句但沒有if條件的情況下袍暴,所得到的輸出的長度將等于所有序列長度的乘積些侍。如果有兩個長度為3的列表隶症,結(jié)果的長度就是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語法上的歧義岗宣,如果expression用來生成一個元組蚂会,則必須將其用()括起來。下面例子中第一個有語法錯誤狈定,第二個是正確的颂龙。

# 語法錯誤
[ x,y for x in seq1 for y in seq2]
# 正確
[ (x,y) for x in seq1 for y in seq2]

生成器

生成器是特殊的一類函數(shù),用于簡化迭代器的編寫纽什。常規(guī)的函數(shù)計算一個值并返回措嵌,但是生成器會返回返回值的迭代器。

你肯定對Python或者C語言如果調(diào)用函數(shù)很熟悉芦缰。當(dāng)一個函數(shù)被調(diào)用時企巢,它會創(chuàng)建一個私有的命名空間,在這個空間內(nèi)創(chuàng)建局部變量让蕾。當(dāng)函數(shù)執(zhí)行到return語句時浪规,局部變量被銷毀,并且將結(jié)果返回給調(diào)用者探孝。該函數(shù)的下一次被調(diào)用時笋婿,它會創(chuàng)建一個新的私有命名空間和局部變量。但是顿颅,如果局部變量在退出時沒有被銷毀缸濒,該怎么辦呢?如果想過一段時候在上次未執(zhí)行完的地方繼續(xù)執(zhí)行粱腻,該怎么辦呢庇配?這就要提到生成器了;它們被認(rèn)為是可恢復(fù)執(zhí)行的函數(shù)绍些。

下面是一個最簡單的生成器函數(shù)的例子:

def generate_ints(N):
    for i in range(N):
        yield i

任何含有yield關(guān)鍵字的函數(shù)都被認(rèn)為是生成器函數(shù)捞慌;這是由Python字節(jié)碼編譯器檢測到的,編譯器特別編譯了該函數(shù)柬批。

當(dāng)生成器函數(shù)被調(diào)用時啸澡,它不會返回單個值,而是會返回支持迭代器協(xié)議的生成器對象氮帐。在執(zhí)行yield語句時锻霎,生成器輸出i的值,這和return語句類似揪漩。yieldreturn語句之間的最大區(qū)別在于旋恼,在到達(dá)yield時,生成器的執(zhí)行狀態(tài)將被暫停,并保留局部變量冰更。在下一次調(diào)用生成器的.next()方法時产徊,該函數(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語句只能在沒有值的情況下使用,并且表示值的結(jié)束奠衔。在執(zhí)行return語句后谆刨,生成器不能再返回更多的值。如果在生成器函數(shù)中return帶了返回值归斤,比如return 5痊夭,會被認(rèn)為是語法錯誤≡嗬铮可以通過手動拋出StopIteration異常她我,或者讓函數(shù)執(zhí)行到最后,來讓生成器函數(shù)不再產(chǎn)生新的值迫横。
可以通過自定義的類番舆,將生成器的所有本地變量存儲為實(shí)例,來達(dá)到生成器的效果矾踱。比如恨狈,通過將self.count設(shè)置為0,將self.next()實(shí)現(xiàn)為自增self.count并返回呛讲,來返回一列整數(shù)禾怠。然而,對于一個有一定復(fù)雜度的生成器圣蝎,實(shí)現(xiàn)自定義的類會更麻煩刃宵。

Python測試套件中的test_generator.py有更多有意思的例子衡瓶。下面是一個遞歸實(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

另外兩個例子提供了N皇后問題(在N*N棋盤上放N個皇后使其彼此不會互相威脅)和騎士之旅的解法。

向生成器傳值

在Python2.4及之前的版本中哮针,生成器只能產(chǎn)生輸出关面。一旦生成器的代碼被調(diào)用生成一個迭代器,當(dāng)函數(shù)恢復(fù)執(zhí)行時十厢,沒有辦法將任何新的信息傳遞到函數(shù)中等太。可以通過讓生成器檢查全局變量蛮放,或者傳遞一些可變對象供調(diào)用方修改缩抡,來將新信息傳入,但是這樣很不優(yōu)雅包颁。

在Python2.5中瞻想,有一個簡單的方法將值傳遞給生成器压真。yield成為了表達(dá)式,返回一個可以分配給變量或者以其他方式運(yùn)行的值蘑险。

val = (yield i)

我推薦大家在執(zhí)行和返回值相關(guān)的操作時滴肿,始終將yield表達(dá)式括起來,如上面的例子佃迄。這個括號并不總是必須的泼差,但是總是添加它比記住何時需要它們更容易。

通過調(diào)用send(value)方法呵俏,可以將值傳到生成器中堆缘。這個方法繼續(xù)執(zhí)行生成器的代碼,yield表達(dá)式會返回傳入的值柴信。如果next()方法被調(diào)用套啤,yield返回None

下面是一個每次增加1的簡單計數(shù)器随常,允許修改內(nèi)部計數(shù)器的值潜沦。

def counter(maximum):
    i = 0
    while i < maximum:
        val = (yield i)
        # 如果給定value,改變計數(shù)器
        if val is not None:
            i = val
        else:
            i += 1

下面是改變計數(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會經(jīng)常返回None绪氛,代碼中應(yīng)該檢查這種情況唆鸡。不要在表達(dá)式中使用這個值,除非確定send()是恢復(fù)函數(shù)的唯一方法枣察。

除了send()争占,生成器還有兩個新的方法:

  • throw(type, value=None, traceback=None)用于在生成器中拋出異常;當(dāng)生成器暫停執(zhí)行時序目,由yield語句拋出這個異常臂痕。
  • close()通過在生成器中拋出GeneratorExit異常來終止迭代。在拋出這個異常后猿涨,生成器代碼必須拋出GeneratorExit或者StopIteration握童;捕獲這個異常是非法的,會觸發(fā)RuntimeError叛赚。在Python的垃圾回收器對生成器進(jìn)行回收時也會調(diào)用close()澡绩。

處理GeneratorExit異常推薦使用try: ... finally:而不是catch GeneratorExit

上面的改動讓生成器由單向生產(chǎn)者變?yōu)樯a(chǎn)者和消費(fèi)者俺附。

生成器也成為了協(xié)程--一種更通用的子程序肥卡。子程序在一個點(diǎn)進(jìn)入,在另外點(diǎn)退出(函數(shù)的入口和return語句)事镣,但是協(xié)程可以在多個不同的點(diǎn)進(jìn)入步鉴,退出,恢復(fù)執(zhí)行(yield語句)。

內(nèi)置函數(shù)

現(xiàn)在來看看迭代器常用的內(nèi)置函數(shù)氛琢。

兩個Python內(nèi)置的函數(shù)map()filter()在某種程度上已經(jīng)被淘汰了只嚣;它們的功能和列表推導(dǎo)式重合,不過返回的是實(shí)際的列表艺沼,而不是迭代器册舞。

map(f, iterA, iterB, ...)返回一個列表,內(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()完成一樣的功能挽荡,不過它可以處理無盡的迭代器藐石;等會在討論itertools模塊的再談。

filter(predicate, iter)返回一個包含滿足特定條件的所有序列元素的列表定拟,這個功能和列表推導(dǎo)重復(fù)于微。predicate是返回一些條件的真值的函數(shù);為了和filter()一起使用青自,predicate只能傳入一個參數(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模塊中也有對應(yīng)的方法,itertools.ifilter延窜,這個方法返回一個迭代器恋腕,因此也可以像itertools.imap()一樣處理無限序列。

reduce(func, iter, [initial_value])itertools模塊中沒有對應(yīng)的方法逆瑞,因?yàn)樗鄯e地對可迭代對象的所有元素執(zhí)行操作荠藤,因此不能用于無限迭代。func函數(shù)必須接受兩個參數(shù)并返回一個值获高。reduce()接受迭代器返回的前兩個元素A和B哈肖,返回func(A, B)。之后請求第三個元素C念秧,計算func(func(A, B), C)淤井,然后請求迭代器返回的第四個元素,持續(xù)這樣的步驟一直到迭代完所有元素出爹。如果可迭代對象不返回任何元素庄吼,會拋出TypeError異常缎除。如果提供了初始值严就,第一輪運(yùn)算會是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()器罐,會得到迭代對象所有值的和梢为。這個場景使用非常廣泛,有內(nèi)置的sum()方法來計算。

>>> reduce(operator.add, [1,2,3,4], 0)
10
>>> sum([1,2,3,4])
10
>>> sum([])
0

在很多reduce()的使用場景中铸董,使用for循環(huán)會更好祟印。

product = reduce(operator.mul, [1,2,3], 1)

# 等同于
product = 1
for i in [1,2,3]:
    product *= i

enumerate(iter)對可迭代對象中的元素進(jìn)行計數(shù),返回計數(shù)值和沒有元素的二元組粟害。

>>> for item in enumerate(['subject', 'verb', 'object']):
...     print item
...
(0, 'subject')
(1, 'verb')
(2, 'object')

循環(huán)遍歷列表并記錄滿足某些條件的索引時蕴忆,常常使用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])將可迭代對象中的所有元素收集到列表中,對列表進(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ù)查看一個可迭代對象的真值汰具。如果有任意元素為真值卓鹿,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ù)式程序時,通常需要很少的功能作為謂詞或以某種方式組合元素聚蝶。

如果有一個Python內(nèi)置函數(shù)或是模塊是合適的杰妓,那么就不需要重新定義一個新的功能:

stripped_lines = [line.strip() for line in lines]
existing_lines = filter(os.path.exists, file_list)

如果不存在現(xiàn)有的函數(shù),那么你需要實(shí)現(xiàn)一個碘勉。編寫小函數(shù)的一種方法是使用lambda語句稚失。lambda需要一些參數(shù)和處理這些參數(shù)的表達(dá)式,并創(chuàng)建一個返回表達(dá)式值的小函數(shù):

lowercase = lambda x: x.lower()
print_assign = lambda name, value: name + '=' + str(value)
adder = lambda x, y: x+y

一種替代的方法是用def定義一個函數(shù):

def lowercase(x):
    return x.lower()
    
def print_assign(name, value):
    return name + '=' + str(value)
    
def adder(x, y):
    return x + y

哪種方式更好呢恰聘?這是一個編程風(fēng)格的問題句各;我通常的做法是避免使用lambda

原因是lambda在可以定義的功能上是非常有限的晴叨。結(jié)果必須作為單個表達(dá)式計算凿宾,這意味著不能使用if... elif... elsetry... except語句。如果試圖在lambda語句中做太多的事情兼蕊,那么最終會出現(xiàn)一個難以理解的過于復(fù)雜的表達(dá)式初厚。能快速說出下面代碼的作用嗎?

total = reduce(lambda a, b: (0, a[1]+b[1]), items)[1]

需要一些時間來弄清楚表達(dá)方式孙技,才能弄清楚代碼想要干什么产禾。使用一個簡短的嵌套的def語句會好一些:

def combine(a, b):
    return 0, a[1] + b[1]

total = reduce(combine, items)[1]

如果只用一個for循環(huán)就更好了。

total = 0
for a, b in items:
    total += b

或者使用內(nèi)置的sum()和生成器表達(dá)式

total = sum(b for a, b in items)

很多時候使用for循環(huán)比使用reduce()代碼更加清晰牵啦。

Fredrik Lundh曾經(jīng)提出過以下lambda重構(gòu)的規(guī)則:

  1. 寫一個lambda函數(shù)亚情。
  2. 寫一個注釋,說明該lambda函數(shù)的作用哈雏。
  3. 研究注釋一段時間楞件,并想出一個名字來捕捉評論的本職衫生。
  4. 使用該名稱將lambda函數(shù)轉(zhuǎn)換為def語句。
  5. 移除注釋土浸。

我很喜歡這些規(guī)則罪针,但是你可以有自己的選擇。

itertools模塊

itertools模塊包含許多常用的迭代器黄伊,以及用于組合幾個迭代器的函數(shù)泪酱。本節(jié)將通過幾個小例子來介紹模塊的內(nèi)容。

該模塊的功能分為幾類:

  • 基于現(xiàn)有迭代器創(chuàng)建新迭代器的函數(shù)还最。
  • 將迭代器作為函數(shù)的參數(shù)西篓。
  • 用于選擇迭代器部分輸出的函數(shù)。
  • 對迭代器輸出進(jìn)行分組的函數(shù)憋活。

創(chuàng)建新的迭代器

函數(shù)itertools.count(n)返回?zé)o限的整數(shù)流岂津,每次增加1≡眉矗可以選擇起始的數(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)保存可迭代對象內(nèi)容的副本,返回一個新的迭代器辜梳,該迭代器從頭到尾返回其元素粱甫。新的迭代器將無限重復(fù)這些元素。

itertools.cycle([1,2,3,4,5]) =>
    1, 2, 3, 4, 5, 1, 2, 3, 4, 5, ...

itertools.repeat(elem, [n])對傳入的元素重復(fù)n次作瞄,如果n沒有提供茶宵,則無限返回該元素。

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ù)量的可迭代對象宗挥,返回第一個迭代器的所有元素乌庶,然后返回第二個迭代器的所有元素,依次類推契耿,知道返回最有一個迭代器的所有元素瞒大。

itertools.chain(['a', 'b', 'c'], (1, 2, 3)) =>
    a, b, c, 1, 2, 3

itertools.izip(iterA, iterB, ...)每次從可迭代對象中讀取一個元素,然后在元組中返回它們:

itertools.izip(['a', 'b', 'c'], (1, 2, 3)) =>
    ('a', 1), ('b', 2), ('c', 3)

這個和內(nèi)置的zip()函數(shù)類似搪桂,但是不構(gòu)成內(nèi)存列表透敌,并在返回之前耗盡所有的輸入迭代器;只有當(dāng)它們被請求時踢械,才構(gòu)造并返回元組酗电。(專業(yè)術(shù)語叫做惰性求值)。

該迭代器旨在于所有長度相同的可迭代對象使用内列。如果可迭代對象長度不同撵术,生成的流將與最短的可迭代對象長度相同。

itertools.izip(['a', 'b'], (1, 2, 3)) =>
    ('a', 1), ('b', 2)

但是應(yīng)該避免這樣做德绿,因?yàn)閺母L的可迭代對象中讀取的元素可能會被丟棄荷荤。這意味著不能進(jìn)一步使用該可迭代對象,因?yàn)橛刑^丟棄元素的風(fēng)險移稳。

itertools.islice(iter, [start], stop, [step])返回迭代器的切片流蕴纳。它會在遇到第一個stop的元素時返回。如果提供了起始索引个粱,將會獲得stop-start之間的元素古毛,如果提供了step值,將會相應(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ù)制一個迭代器胶征;它返回n個獨(dú)立的迭代器塞椎,它們將返回源迭代器的內(nèi)容。n的默認(rèn)值是2睛低。復(fù)制迭代器需要保存源迭代器的一些內(nèi)容案狠,因此如果源迭代器很大并且一個新的迭代器比其他迭代器更多被消費(fèi),那么這會消耗很可觀的內(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, ...

對可迭代對象的元素進(jìn)行操作

有兩個函數(shù)用于對可迭代的內(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)算符相對應(yīng)的函數(shù)。比如operator.add(a, b)(兩個元素相加)罩抗,operator.ne(a, b)(等同于a!=b)拉庵,和operator.attrgetter('id')(返回一個可以獲取"id"屬性的可調(diào)用方法)。

itertools.starmap(func, iter)假定可迭代對象返回一個元組流套蒂,并使用這些元組作為參數(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)在談的最后一個函數(shù)itertools.groupby(iter, key_func=None)是最復(fù)雜的卡啰。key_func(elem)是一個可以計算可迭代對象返回的每個元素鍵值的函數(shù)静稻。如果沒有傳入這個函數(shù),鍵值就是元素本身匈辱。

函數(shù)groupby()從具有相同鍵值的底層迭代中收集所有連續(xù)元素振湾,并返回一個包含鍵值的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()假定底層可迭代對象的內(nèi)容已經(jīng)根據(jù)鍵值進(jìn)行排序亡脸。請注意押搪,返回的迭代器依然使用底層的可迭代對象树酪,因此必須在消費(fèi)iterator-1中的內(nèi)容后,再去請求iterator-2及其相應(yīng)的鍵值大州。

functools模塊

在Python2.5中引入的functools模塊包含一些高階函數(shù)续语。高階函數(shù)將一個或多個函數(shù)作為輸入,并返回一個新的函數(shù)厦画。其中最有用的就是functools.partial()函數(shù)疮茄。

對函數(shù)式風(fēng)格的程序來說,有時要構(gòu)建具有填充一些參數(shù)的現(xiàn)有函數(shù)的變體根暑。比如函數(shù)f(a, b, c)力试;你可能希望創(chuàng)建一個新的函數(shù)g(b, c),相當(dāng)于f(1, b, c)排嫌;這被稱之為“部分功能應(yīng)用程序”畸裳。

partial的構(gòu)造函數(shù)使用參數(shù)(function, arg1, arg2, ... kwarg1=value1, kwarg2=value2)。生成的對象是可調(diào)用的淳地,所以可以用它來構(gòu)造可以填充參數(shù)的函數(shù)躯畴。

下面是一個小例子:

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操作符相對應(yīng)的函數(shù)薇芝。這些函數(shù)在函數(shù)式風(fēng)格的代碼中通常很有用蓬抄,因?yàn)樗梢蕴娲恍┲话▎蝹€操作的函數(shù)。

其中的一些函數(shù)有:

  • 數(shù)學(xué)運(yùn)算:add(),sub(),mul(),div(),floordiv(),abs(),...
  • 邏輯運(yùn)算:not_(),truth()夯到。
  • 位運(yùn)算:and_(),or_(),invert()嚷缭。
  • 對比:eq(),ne(),lt(),le(),gt()ge()
  • 對象標(biāo)識:is_(),is_not()耍贾。

請參閱operator模塊的文檔來獲取完整列表阅爽。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市荐开,隨后出現(xiàn)的幾起案子付翁,更是在濱河造成了極大的恐慌,老刑警劉巖晃听,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件百侧,死亡現(xiàn)場離奇詭異,居然都是意外死亡能扒,警方通過查閱死者的電腦和手機(jī)佣渴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來初斑,“玉大人鲜侥,你說我怎么就攤上這事蛛砰。” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵必尼,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么突硝? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮三圆,結(jié)果婚禮上狞换,老公的妹妹穿的比我還像新娘避咆。我一直安慰自己舟肉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布查库。 她就那樣靜靜地躺著路媚,像睡著了一般。 火紅的嫁衣襯著肌膚如雪樊销。 梳的紋絲不亂的頭發(fā)上整慎,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機(jī)與錄音围苫,去河邊找鬼裤园。 笑死,一個胖子當(dāng)著我的面吹牛剂府,可吹牛的內(nèi)容都是我干的拧揽。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼腺占,長吁一口氣:“原來是場噩夢啊……” “哼淤袜!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起衰伯,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤铡羡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后意鲸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體烦周,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年怎顾,在試婚紗的時候發(fā)現(xiàn)自己被綠了论矾。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡杆勇,死狀恐怖贪壳,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蚜退,我是刑警寧澤闰靴,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布彪笼,位于F島的核電站,受9級特大地震影響蚂且,放射性物質(zhì)發(fā)生泄漏配猫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一杏死、第九天 我趴在偏房一處隱蔽的房頂上張望泵肄。 院中可真熱鬧,春花似錦淑翼、人聲如沸腐巢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽冯丙。三九已至,卻和暖如春遭京,著一層夾襖步出監(jiān)牢的瞬間胃惜,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工哪雕, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留船殉,地道東北人。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓斯嚎,卻偏偏與公主長得像利虫,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子孝扛,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容