函數(shù)式編程指引?

作者

A. M. Kuchling

發(fā)布版本

0.32

本文檔提供恰當(dāng)?shù)?Python 函數(shù)式編程范例,在函數(shù)式編程簡(jiǎn)單的介紹之后,將簡(jiǎn)單介紹Python中關(guān)于函數(shù)式編程的特性如 iteratorgenerator 以及相關(guān)庫(kù)模塊如 itertoolsfunctools 等。

概述?

本章介紹函數(shù)式編程的基本概念。如您僅想學(xué)習(xí) Python 語(yǔ)言的特性,可跳過(guò)本章直接查看 迭代器.

編程語(yǔ)言支持通過(guò)以下幾種方式來(lái)解構(gòu)具體問(wèn)題:

  • 大多數(shù)的編程語(yǔ)言都是 過(guò)程式 的,所謂程序就是一連串告訴計(jì)算機(jī)怎樣處理程序輸入的指令。C、Pascal 甚至 Unix shells 都是過(guò)程式語(yǔ)言。

  • 聲明式 語(yǔ)言中,你編寫一個(gè)用來(lái)描述待解決問(wèn)題的說(shuō)明,并且這個(gè)語(yǔ)言的具體實(shí)現(xiàn)會(huì)指明怎樣高效的進(jìn)行計(jì)算。 SQL 可能是你最熟悉的聲明式語(yǔ)言了。 一個(gè) SQL 查詢語(yǔ)句描述了你想要檢索的數(shù)據(jù)集,并且 SQL 引擎會(huì)決定是掃描整張表還是使用索引,應(yīng)該先執(zhí)行哪些子句等等。

  • 面向?qū)ο?/strong> 程序會(huì)操作一組對(duì)象。 對(duì)象擁有內(nèi)部狀態(tài),并能夠以某種方式支持請(qǐng)求和修改這個(gè)內(nèi)部狀態(tài)的方法。Smalltalk 和 Java 都是面向?qū)ο蟮恼Z(yǔ)言。 C++ 和 Python 支持面向?qū)ο缶幊?,但并不?qiáng)制使用面向?qū)ο筇匦浴?/p>

  • 函數(shù)式 編程則將一個(gè)問(wèn)題分解成一系列函數(shù)。 理想情況下,函數(shù)只接受輸入并輸出結(jié)果,對(duì)一個(gè)給定的輸入也不會(huì)有影響輸出的內(nèi)部狀態(tài)。 著名的函數(shù)式語(yǔ)言有 ML 家族(Standard ML,Ocaml 以及其他變種)和 Haskell。

一些語(yǔ)言的設(shè)計(jì)者選擇強(qiáng)調(diào)一種特定的編程方式。 這通常會(huì)讓以不同的方式來(lái)編寫程序變得困難。其他多范式語(yǔ)言則支持幾種不同的編程方式。Lisp,C++ 和 Python 都是多范式語(yǔ)言;使用這些語(yǔ)言,你可以編寫主要為過(guò)程式,面向?qū)ο蠡蛘吆瘮?shù)式的程序和函數(shù)庫(kù)。在大型程序中,不同的部分可能會(huì)采用不同的方式編寫;比如 GUI 可能是面向?qū)ο蟮亩幚磉壿媱t是過(guò)程式或者函數(shù)式。

在函數(shù)式程序里,輸入會(huì)流經(jīng)一系列函數(shù)。每個(gè)函數(shù)接受輸入并輸出結(jié)果。函數(shù)式風(fēng)格反對(duì)使用帶有副作用的函數(shù),這些副作用會(huì)修改內(nèi)部狀態(tài),或者引起一些無(wú)法體現(xiàn)在函數(shù)的返回值中的變化。完全不產(chǎn)生副作用的函數(shù)被稱作“純函數(shù)”。消除副作用意味著不能使用隨程序運(yùn)行而更新的數(shù)據(jù)結(jié)構(gòu);每個(gè)函數(shù)的輸出必須只依賴于輸入。

Some languages are very strict about purity and don't even have assignment statements such as a=3 or c = a + b, but it's difficult to avoid all side effects, such as printing to the screen or writing to a disk file. Another example is a call to the print() or time.sleep() function, neither of which returns a useful value. Both are called only for their side effects of sending some text to the screen or pausing execution for a second.

函數(shù)式風(fēng)格的 Python 程序并不會(huì)極端到消除所有 I/O 或者賦值的程度;相反,他們會(huì)提供像函數(shù)式一樣的接口,但會(huì)在內(nèi)部使用非函數(shù)式的特性。比如,函數(shù)的實(shí)現(xiàn)仍然會(huì)使用局部變量,但不會(huì)修改全局變量或者有其他副作用。

函數(shù)式編程可以被認(rèn)為是面向?qū)ο缶幊痰膶?duì)立面。對(duì)象就像是顆小膠囊,包裹著內(nèi)部狀態(tài)和隨之而來(lái)的能讓你修改這個(gè)內(nèi)部狀態(tài)的一組調(diào)用方法,以及由正確的狀態(tài)變化所構(gòu)成的程序。函數(shù)式編程希望盡可能地消除狀態(tài)變化,只和流經(jīng)函數(shù)的數(shù)據(jù)打交道。在 Python 里你可以把兩種編程方式結(jié)合起來(lái),在你的應(yīng)用(電子郵件信息,事務(wù)處理)中編寫接受和返回對(duì)象實(shí)例的函數(shù)。

函數(shù)式設(shè)計(jì)在工作中看起來(lái)是個(gè)奇怪的約束。為什么你要消除對(duì)象和副作用呢?不過(guò)函數(shù)式風(fēng)格有其理論和實(shí)踐上的優(yōu)點(diǎn):

  • 形式證明。

  • 模塊化。

  • 組合性。

  • 易于調(diào)試和測(cè)試。

形式證明?

一個(gè)理論上的優(yōu)點(diǎn)是,構(gòu)造數(shù)學(xué)證明來(lái)說(shuō)明函數(shù)式程序是正確的相對(duì)更容易些。

很長(zhǎng)時(shí)間,研究者們對(duì)尋找證明程序正確的數(shù)學(xué)方法都很感興趣。這和通過(guò)大量輸入來(lái)測(cè)試,并得出程序的輸出基本正確,或者閱讀一個(gè)程序的源代碼然后得出代碼看起來(lái)沒(méi)問(wèn)題不同;相反,這里的目標(biāo)是一個(gè)嚴(yán)格的證明,證明程序?qū)λ锌赡艿妮斎攵寄芙o出正確的結(jié)果。

證明程序正確性所用到的技術(shù)是寫出 不變量,也就是對(duì)于輸入數(shù)據(jù)和程序中的變量永遠(yuǎn)為真的特性。然后對(duì)每行代碼,你說(shuō)明這行代碼執(zhí)行前的不變量 X 和 Y 以及執(zhí)行后稍有不同的不變量 X' 和 Y' 為真。如此一直到程序結(jié)束,這時(shí)候在程序的輸出上,不變量應(yīng)該會(huì)與期望的狀態(tài)一致。

函數(shù)式編程之所以要消除賦值,是因?yàn)橘x值在這個(gè)技術(shù)中難以處理;賦值可能會(huì)破壞賦值前為真的不變量,卻并不產(chǎn)生任何可以傳遞下去的新的不變量。

不幸的是,證明程序的正確性很大程度上是經(jīng)驗(yàn)性質(zhì)的,而且和 Python 軟件無(wú)關(guān)。即使是微不足道的程序都需要幾頁(yè)長(zhǎng)的證明;一個(gè)中等復(fù)雜的程序的正確性證明會(huì)非常龐大,而且,極少甚至沒(méi)有你日常所使用的程序(Python 解釋器,XML 解析器,瀏覽器)的正確性能夠被證明。即使你寫出或者生成一個(gè)證明,驗(yàn)證證明也會(huì)是一個(gè)問(wèn)題;里面可能出了差錯(cuò),而你錯(cuò)誤地相信你證明了程序的正確性。

模塊化?

函數(shù)式編程的一個(gè)更實(shí)用的優(yōu)點(diǎn)是,它強(qiáng)制你把問(wèn)題分解成小的方面。因此程序會(huì)更加模塊化。相對(duì)于一個(gè)進(jìn)行了復(fù)雜變換的大型函數(shù),一個(gè)小的函數(shù)更明確,更易于編寫, 也更易于閱讀和檢查錯(cuò)誤。

易于調(diào)試和測(cè)試?

測(cè)試和調(diào)試函數(shù)式程序相對(duì)來(lái)說(shuō)更容易。

調(diào)試很簡(jiǎn)單是因?yàn)楹瘮?shù)通常都很小而且清晰明確。當(dāng)程序無(wú)法工作的時(shí)候,每個(gè)函數(shù)都是一個(gè)可以檢查數(shù)據(jù)是否正確的接入點(diǎn)。你可以通過(guò)查看中間輸入和輸出迅速找到出錯(cuò)的函數(shù)。

測(cè)試更容易是因?yàn)槊總€(gè)函數(shù)都是單元測(cè)試的潛在目標(biāo)。在執(zhí)行測(cè)試前,函數(shù)并不依賴于需要重現(xiàn)的系統(tǒng)狀態(tài);相反,你只需要給出正確的輸入,然后檢查輸出是否和期望的結(jié)果一致。

組合性?

當(dāng)你編寫函數(shù)式風(fēng)格的程序時(shí),你會(huì)寫出很多帶有不同輸入和輸出的函數(shù)。其中一些不可避免地會(huì)局限于特定的應(yīng)用,但其他的卻可以廣泛的用在程序中。舉例來(lái)說(shuō),一個(gè)接受文件夾目錄返回所有文件夾中的 XML 文件的函數(shù); 或是一個(gè)接受文件名,然后返回文件內(nèi)容的函數(shù),都可以應(yīng)用在很多不同的場(chǎng)合。

久而久之你會(huì)形成一個(gè)個(gè)人工具庫(kù)。通常你可以重新組織已有的函數(shù)來(lái)組成新的程序,然后為當(dāng)前的工作寫一些特殊的函數(shù)。

迭代器?

我會(huì)從 Python 的一個(gè)語(yǔ)言特性, 編寫函數(shù)式風(fēng)格程序的重要基石開(kāi)始說(shuō)起:迭代器。

迭代器是一個(gè)表示數(shù)據(jù)流的對(duì)象;這個(gè)對(duì)象每次只返回一個(gè)元素。Python 迭代器必須支持 __next__() 方法;這個(gè)方法不接受參數(shù),并總是返回?cái)?shù)據(jù)流中的下一個(gè)元素。如果數(shù)據(jù)流中沒(méi)有元素,__next__() 會(huì)拋出 StopIteration 異常。迭代器未必是有限的;完全有理由構(gòu)造一個(gè)輸出無(wú)限數(shù)據(jù)流的迭代器。

內(nèi)置的 iter() 函數(shù)接受任意對(duì)象并試圖返回一個(gè)迭代器來(lái)輸出對(duì)象的內(nèi)容或元素,并會(huì)在對(duì)象不支持迭代的時(shí)候拋出 TypeError 異常。Python 有幾種內(nèi)置數(shù)據(jù)類型支持迭代,最常見(jiàn)的就是列表和字典。如果一個(gè)對(duì)象能生成迭代器,那么它就會(huì)被稱作 iterable

你可以手動(dòng)試驗(yàn)迭代器的接口。

>>>
>>> L = [1, 2, 3]
>>> it = iter(L)
>>> it  
<...iterator object at ...>
>>> it.__next__()  # same as next(it)
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>

Python 有不少要求使用可迭代的對(duì)象的地方,其中最重要的就是 for 表達(dá)式。在表達(dá)式 for X in Y,Y 要么自身是一個(gè)迭代器,要么能夠由 iter() 創(chuàng)建一個(gè)迭代器。以下兩種表達(dá)是等價(jià)的:

for i in iter(obj):
    print(i)

for i in obj:
    print(i)

可以用 list()tuple() 這樣的構(gòu)造函數(shù)把迭代器具體化成列表或元組:

>>>
>>> L = [1, 2, 3]
>>> iterator = iter(L)
>>> t = tuple(iterator)
>>> t
(1, 2, 3)

序列的解壓操作也支持迭代器:如果你知道一個(gè)迭代器能夠返回 N 個(gè)元素,你可以把他們解壓到有 N 個(gè)元素的元組:

>>>
>>> L = [1, 2, 3]
>>> iterator = iter(L)
>>> a, b, c = iterator
>>> a, b, c
(1, 2, 3)

max()min() 這樣的內(nèi)置函數(shù)可以接受單個(gè)迭代器參數(shù),然后返回其中最大或者最小的元素。"in""not in" 操作也支持迭代器:如果能夠在迭代器 iterator 返回的數(shù)據(jù)流中找到 X 的話,則``X in iterator`` 為真。很顯然,如果迭代器是無(wú)限的,這么做你就會(huì)遇到問(wèn)題;max()min() 永遠(yuǎn)也不會(huì)返回;如果元素 X 也不出現(xiàn)在數(shù)據(jù)流中,"in""not in" 操作同樣也永遠(yuǎn)不會(huì)返回。

注意你只能在迭代器中順序前進(jìn);沒(méi)有獲取前一個(gè)元素的方法,除非重置迭代器,或者重新復(fù)制一份。迭代器對(duì)象可以提供這些額外的功能,但迭代器協(xié)議只明確了 __next__() 方法。函數(shù)可能因此而耗盡迭代器的輸出,如果你要對(duì)同樣的數(shù)據(jù)流做不同的操作,你必須重新創(chuàng)建一個(gè)迭代器。

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

我們已經(jīng)知道列表和元組支持迭代器。實(shí)際上,Python 中的任何序列類型,比如字符串,都自動(dòng)支持創(chuàng)建迭代器。

對(duì)字典調(diào)用 iter() 會(huì)返回一個(gè)遍歷字典的鍵的迭代器:

>>>
>>> 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])
Jan 1
Feb 2
Mar 3
Apr 4
May 5
Jun 6
Jul 7
Aug 8
Sep 9
Oct 10
Nov 11
Dec 12

注意從 Python 3.7 開(kāi)始,字典的遍歷順序一定和輸入順序一樣。先前的版本并沒(méi)有明確這一點(diǎn),所以不同的實(shí)現(xiàn)可能不一致。

對(duì)字典使用 iter() 總是會(huì)遍歷鍵,但字典也有返回其他迭代器的方法。如果你只遍歷值或者鍵/值對(duì),你可以明確地調(diào)用 values()items() 方法得到合適的迭代器。

dict() 構(gòu)造函數(shù)可以接受一個(gè)迭代器,然后返回一個(gè)有限的 (key, value) 元組的數(shù)據(jù)流:

>>>
>>> L = [('Italy', 'Rome'), ('France', 'Paris'), ('US', 'Washington DC')]
>>> dict(iter(L))
{'Italy': 'Rome', 'France': 'Paris', 'US': 'Washington DC'}

文件也可以通過(guò)調(diào)用 readline() 來(lái)遍歷,直到窮盡文件中所有的行。這意味著你可以像這樣讀取文件中的每一行:

for line in file:
    # do something for each line
    ...

集合可以從可遍歷的對(duì)象獲取內(nèi)容,也可以讓你遍歷集合的元素:

S = {2, 3, 5, 7, 11, 13}
for i in S:
    print(i)

生成器表達(dá)式和列表推導(dǎo)式?

迭代器的輸出有兩個(gè)很常見(jiàn)的使用方式,1) 對(duì)每一個(gè)元素執(zhí)行操作,2) 選擇一個(gè)符合條件的元素子集。比如,給定一個(gè)字符串列表,你可能想去掉每個(gè)字符串尾部的空白字符,或是選出所有包含給定子串的字符串。

列表推導(dǎo)式和生成器表達(dá)時(shí)(簡(jiǎn)寫:"listcomps" 和 "genexps")讓這些操作更加簡(jiǎn)明,這個(gè)形式借鑒自函數(shù)式程序語(yǔ)言 Haskell(https://www.haskell.org/)。你可以用以下代碼去掉一個(gè)字符串流中的所有空白字符:

line_list = ['  line 1\n', 'line 2  \n', ...]

# Generator expression -- returns iterator
stripped_iter = (line.strip() for line in line_list)

# List comprehension -- returns list
stripped_list = [line.strip() for line in line_list]

你可以加上條件語(yǔ)句 "if" 來(lái)選取特定的元素:

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

通過(guò)列表推導(dǎo)式,你會(huì)獲得一個(gè) Python 列表;stripped_list 就是一個(gè)包含所有結(jié)果行的列表,并不是迭代器。 生成器表達(dá)式會(huì)返回一個(gè)迭代器,它在必要的時(shí)候計(jì)算結(jié)果,避免一次性生成所有的值。 這意味著,如果迭代器返回一個(gè)無(wú)限數(shù)據(jù)流或者大量的數(shù)據(jù),列表推導(dǎo)式就不太好用了。 這種情況下生成器表達(dá)式會(huì)更受青睞。

生成器表達(dá)式兩邊使用圓括號(hào) ("()") ,而列表推導(dǎo)式則使用方括號(hà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 )

再次說(shuō)明,列表推導(dǎo)式只有兩邊的括號(hào)不一樣(方括號(hào)而不是圓括號(hào))。

這些生成用于輸出的元素會(huì)成為 expression 的后繼值。其中 if 語(yǔ)句是可選的;如果給定的話 expression 只會(huì)在符合條件時(shí)計(jì)算并加入到結(jié)果中。

生成器表達(dá)式總是寫在圓括號(hào)里面,不過(guò)也可以算上調(diào)用函數(shù)時(shí)用的括號(hào)。如果你想即時(shí)創(chuàng)建一個(gè)傳遞給函數(shù)的迭代器,可以這么寫:

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

其中 for...in 語(yǔ)句包含了將要遍歷的序列。這些序列并不必須同樣長(zhǎng),因?yàn)樗鼈儠?huì)從左往右開(kāi)始遍歷,而 不是 同時(shí)執(zhí)行。對(duì)每個(gè) sequence1 中的元素,sequence2 會(huì)從頭開(kāi)始遍歷。sequence3 會(huì)對(duì)每個(gè) sequence1sequence2 的元素對(duì)開(kāi)始遍歷。

換句話說(shuō),列表推導(dǎo)式器是和下面的 Python 代碼等價(jià):

for expr1 in sequence1:
    if not (condition1):
        continue   # Skip this element
    for expr2 in sequence2:
        if not (condition2):
            continue   # Skip this element
        ...
        for exprN in sequenceN:
            if not (conditionN):
                continue   # Skip this element

            # Output the value of
            # the expression.

這說(shuō)明,如果有多個(gè) for...in 語(yǔ)句而沒(méi)有 if 語(yǔ)句,輸出結(jié)果的長(zhǎng)度就是所有序列長(zhǎng)度的乘積。如果你的兩個(gè)列表長(zhǎng)度為3,那么輸出的列表長(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 會(huì)生成元組,那這個(gè)元組必須要用括號(hào)括起來(lái)。下面第一個(gè)列表推導(dǎo)式語(yǔ)法錯(cuò)誤,第二個(gè)則是正確的:

# Syntax error
[x, y for x in seq1 for y in seq2]
# Correct
[(x, y) for x in seq1 for y in seq2]

生成器?

生成器是一類用來(lái)簡(jiǎn)化編寫迭代器工作的特殊函數(shù)。普通的函數(shù)計(jì)算并返回一個(gè)值,而生成器返回一個(gè)能返回?cái)?shù)據(jù)流的迭代器。

毫無(wú)疑問(wèn),你已經(jīng)對(duì)如何在 Python 和 C 中調(diào)用普通函數(shù)很熟悉了,這時(shí)候函數(shù)會(huì)獲得一個(gè)創(chuàng)建局部變量的私有命名空間。當(dāng)函數(shù)到達(dá) return 表達(dá)式時(shí),局部變量會(huì)被銷毀然后把返回給調(diào)用者。之后調(diào)用同樣的函數(shù)時(shí)會(huì)創(chuàng)建一個(gè)新的私有命名空間和一組全新的局部變量。但是,如果在退出一個(gè)函數(shù)時(shí)不扔掉局部變量會(huì)如何呢?如果稍后你能夠從退出函數(shù)的地方重新恢復(fù)又如何呢?這就是生成器所提供的;他們可以被看成可恢復(fù)的函數(shù)。

這里有簡(jiǎn)單的生成器函數(shù)示例:

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

任何包含了 yield 關(guān)鍵字的函數(shù)都是生成器函數(shù);Python 的 bytecode 編譯器會(huì)在編譯的時(shí)候檢測(cè)到并因此而特殊處理。

當(dāng)你調(diào)用一個(gè)生成器函數(shù),它并不會(huì)返回單獨(dú)的值,而是返回一個(gè)支持生成器協(xié)議的生成器對(duì)象。當(dāng)執(zhí)行 yield 表達(dá)式時(shí),生成器會(huì)輸出 i 的值,就像 return 表達(dá)式一樣。yieldreturn 最大的區(qū)別在于,到達(dá) yield 的時(shí)候生成器的執(zhí)行狀態(tài)會(huì)掛起并保留局部變量。在下一次調(diào)用生成器 __next__() 方法的時(shí)候,函數(shù)會(huì)恢復(fù)執(zhí)行。

這里有一個(gè) generate_ints() 生成器的示例:

>>>
>>> gen = generate_ints(3)
>>> gen  
<generator object generate_ints at ...>
>>> next(gen)
0
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
Traceback (most recent call last):
  File "stdin", line 1, in <module>
  File "stdin", line 2, in generate_ints
StopIteration

同樣,你可以寫出 for i in generate_ints(5),或者 a, b, c = generate_ints(3)。

在生成器函數(shù)里面,return value 會(huì)觸發(fā)從 __next__() 方法拋出 StopIteration(value) 異常。一旦拋出這個(gè)異常,或者函數(shù)結(jié)束,處理數(shù)據(jù)的過(guò)程就會(huì)停止,生成器也不會(huì)再生成新的值。

你可以手動(dòng)編寫自己的類來(lái)達(dá)到生成器的效果,把生成器的所有局部變量作為實(shí)例的成員變量存儲(chǔ)起來(lái)。比如,可以這么返回一個(gè)整數(shù)列表:把 self.count 設(shè)為0,然后通過(guò) count`()。然而,對(duì)于一個(gè)中等復(fù)雜程度的生成器,寫出一個(gè)相應(yīng)的類可能會(huì)相當(dāng)繁雜。

包含在 Python 庫(kù)中的測(cè)試套件 Lib/test/test_generators.py 里有很多非常有趣的例子。這里是一個(gè)用生成器實(shí)現(xiàn)樹(shù)的遞歸中序遍歷示例。:

# A recursive generator that generates Tree leaves in in-order.
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è) test_generators.py 中的例子給出了 N 皇后問(wèn)題(在 NxN 的棋盤上放置 N 個(gè)皇后,任何一個(gè)都不能吃掉另一個(gè)),以及馬的遍歷路線(在NxN 的棋盤上給馬找出一條不重復(fù)的走過(guò)所有格子的路線)的解。

向生成器傳遞值?

在 Python 2.4 及之前的版本中,生成器只產(chǎn)生輸出。一旦調(diào)用生成器的代碼創(chuàng)建一個(gè)迭代器,就沒(méi)有辦法在函數(shù)恢復(fù)執(zhí)行的時(shí)候向它傳遞新的信息。你可以設(shè)法實(shí)現(xiàn)這個(gè)功能,讓生成器引用一個(gè)全局變量或者一個(gè)調(diào)用者可以修改的可變對(duì)象,但是這些方法都很繁雜。

在 Python 2.5 里有一個(gè)簡(jiǎn)單的將值傳遞給生成器的方法。yield 變成了一個(gè)表達(dá)式,返回一個(gè)可以賦給變量或執(zhí)行操作的值:

val = (yield i)

我建議你在處理 yield 表達(dá)式返回值的時(shí)候, 總是 兩邊寫上括號(hào),就像上面的例子一樣。括號(hào)并不總是必須的,但是比起記住什么時(shí)候需要括號(hào),寫出來(lái)會(huì)更容易一點(diǎn)。

PEP 342 解釋了具體的規(guī)則,也就是 yield 表達(dá)式必須括起來(lái),除非是出現(xiàn)在最頂級(jí)的賦值表達(dá)式的右邊。這意味著你可以寫 val = yield i,但是必須在操作的時(shí)候加上括號(hào),就像``val = (yield i) + 12``)

可以調(diào)用 send(value)() <generator.send> 方法向生成器發(fā)送值。這個(gè)方法會(huì)恢復(fù)執(zhí)行生成器的代碼,然后 yield 表達(dá)式返回特定的值。如果調(diào)用普通的 __next__`方法,``yield`() 會(huì)返回 None.

這里有一個(gè)簡(jiǎn)單的每次加1的計(jì)數(shù)器,并允許改變內(nèi)部計(jì)數(shù)器的值。

def counter(maximum):
    i = 0
    while i < maximum:
        val = (yield i)
        # If value provided, change counter
        if val is not None:
            i = val
        else:
            i += 1

這是改變計(jì)數(shù)器的一個(gè)示例

>>>
>>> it = counter(10)  
>>> next(it)  
0
>>> next(it)  
1
>>> it.send(8)  
8
>>> next(it)  
9
>>> next(it)  
Traceback (most recent call last):
  File "t.py", line 15, in <module>
    it.next()
StopIteration

因?yàn)?yield 很多時(shí)候會(huì)返回 None,所以你應(yīng)該總是檢查這個(gè)情況。不要在表達(dá)式中使用 yield 的值,除非你確定 send() 是唯一的用來(lái)恢復(fù)你的生成器函數(shù)的方法。

除了 send() 之外,生成器還有兩個(gè)其他的方法:

  • throw(value) is used to raise an exception inside the generator; the exception is raised by the yield expression where the generator's execution is paused.

  • generator.close() 會(huì)在生成器內(nèi)部拋出 GeneratorExit 異常來(lái)結(jié)束迭代。當(dāng)接收到這個(gè)異常時(shí),生成器的代碼會(huì)拋出 GeneratorExit 或者 StopIteration;捕捉這個(gè)異常作其他處理是非法的,并會(huì)出發(fā) RuntimeErrorclose() 也會(huì)在 Python 垃圾回收器回收生成器的時(shí)候調(diào)用。

    如果你要在 GeneratorExit 發(fā)生的時(shí)候清理代碼,我建議使用 try: ... finally: 組合來(lái)代替 GeneratorExit。

這些改變的累積效應(yīng)是,讓生成器從單向的信息生產(chǎn)者變成了既是生產(chǎn)者,又是消費(fèi)者。

生成器也可以成為 協(xié)程 ,一種更廣義的子過(guò)程形式。子過(guò)程可以從一個(gè)地方進(jìn)入,然后從另一個(gè)地方退出(從函數(shù)的頂端進(jìn)入,從 return 語(yǔ)句退出),而協(xié)程可以進(jìn)入,退出,然后在很多不同的地方恢復(fù)(yield 語(yǔ)句)。

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

我們可以看看迭代器常常用到的函數(shù)的更多細(xì)節(jié)。

Python 內(nèi)置的兩個(gè)函數(shù) map()filter() 復(fù)制了生成器表達(dá)式的兩個(gè)特性:

map(f, iterA, iterB, ...) 返回一個(gè)遍歷序列的迭代器

f(iterA[0], iterB[0]), f(iterA[1], iterB[1]), f(iterA[2], iterB[2]), ....

>>>
>>> def upper(s):
...     return s.upper()
>>>
>>> list(map(upper, ['sentence', 'fragment']))
['SENTENCE', 'FRAGMENT']
>>> [upper(s) for s in ['sentence', 'fragment']]
['SENTENCE', 'FRAGMENT']

你當(dāng)然也可以用列表推導(dǎo)式達(dá)到同樣的效果。

filter(predicate, iter) 返回一個(gè)遍歷序列中滿足指定條件的元素的迭代器,和列表推導(dǎo)式的功能相似。 predicate (謂詞)是一個(gè)在特定條件下返回真值的函數(shù);要使用函數(shù) filter(),謂詞函數(shù)必須只能接受一個(gè)參數(shù)。

>>>
>>> def is_even(x):
...     return (x % 2) == 0
>>>
>>> list(filter(is_even, range(10)))
[0, 2, 4, 6, 8]

這也可以寫成列表推導(dǎo)式:

>>>
>>> list(x for x in range(10) if is_even(x))
[0, 2, 4, 6, 8]

enumerate(iter, start=0) 計(jì)數(shù)可迭代對(duì)象中的元素,然后返回包含每個(gè)計(jì)數(shù)(從 start 開(kāi)始)和元素兩個(gè)值的元組。:

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

enumerate() 常常用于遍歷列表并記錄達(dá)到特定條件時(shí)的下標(biāo):

f = open('data.txt', 'r')
for i, line in enumerate(f):
    if line.strip() == '':
        print('Blank line at line #%i' % i)

sorted(iterable, key=None, reverse=False) 會(huì)將 iterable 中的元素收集到一個(gè)列表中,然后排序并返回結(jié)果。其中 keyreverse 參數(shù)會(huì)傳遞給所創(chuàng)建列表的 sort() 方法。:

>>>
>>> import random
>>> # Generate 8 random numbers between [0, 10000)
>>> rand_list = random.sample(range(10000), 8)
>>> rand_list  
[769, 7953, 9828, 6431, 8442, 9878, 6213, 2207]
>>> sorted(rand_list)  
[769, 2207, 6213, 6431, 7953, 8442, 9828, 9878]
>>> sorted(rand_list, reverse=True)  
[9878, 9828, 8442, 7953, 6431, 6213, 2207, 769]

(對(duì)排序更詳細(xì)的討論可參見(jiàn) 排序指南。)

內(nèi)置函數(shù) any(iter)all(iter) 會(huì)查看一個(gè)可迭代對(duì)象內(nèi)容的邏輯值。any() 在可迭代對(duì)象中任意一個(gè)元素為真時(shí)返回 True,而 all() 在所有元素為真時(shí)返回 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

zip(iterA, iterB, ...) 從每個(gè)可迭代對(duì)象中選取單個(gè)元素組成列表并返回:

zip(['a', 'b', 'c'], (1, 2, 3)) =>
  ('a', 1), ('b', 2), ('c', 3)

它并不會(huì)在內(nèi)存創(chuàng)建一個(gè)列表并因此在返回前而耗盡輸入的迭代器;相反,只有在被請(qǐng)求的時(shí)候元組才會(huì)創(chuàng)建并返回。(這種行為的技術(shù)術(shù)語(yǔ)叫惰性計(jì)算,參見(jiàn) lazy evaluation.)

這個(gè)迭代器設(shè)計(jì)用于長(zhǎng)度相同的可迭代對(duì)象。如果可迭代對(duì)象的長(zhǎng)度不一致,返回的數(shù)據(jù)流的長(zhǎng)度會(huì)和最短的可迭代對(duì)象相同

zip(['a', 'b'], (1, 2, 3)) =>
  ('a', 1), ('b', 2)

然而,你應(yīng)該避免這種情況,因?yàn)樗袕母L(zhǎng)的迭代器中取出的元素都會(huì)被丟棄。這意味著之后你也無(wú)法冒著跳過(guò)被丟棄元素的風(fēng)險(xiǎn)來(lái)繼續(xù)使用這個(gè)迭代器。

itertools 模塊?

itertools 模塊包含很多常用的迭代器以及用來(lái)組合迭代器的函數(shù)。本節(jié)會(huì)用些小的例子來(lái)介紹這個(gè)模塊的內(nèi)容。

這個(gè)模塊里的函數(shù)大致可以分為幾類:

  • 從已有的迭代器創(chuàng)建新的迭代器的函數(shù)。

  • 接受迭代器元素作為參數(shù)的函數(shù)。

  • 選取部分迭代器輸出的函數(shù)。

  • 給迭代器輸出分組的函數(shù)。

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

itertools.count(start, step) 返回一個(gè)等分的無(wú)限數(shù)據(jù)流。初始值默認(rèn)為0,間隔默認(rèn)為1,你也選擇可以指定初始值和間隔:

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, ...
itertools.count(10, 5) =>
  10, 15, 20, 25, 30, 35, 40, 45, 50, 55, ...

itertools.cycle(iter) 保存一份所提供的可迭代對(duì)象的副本,并返回一個(gè)能產(chǎn)生整個(gè)可迭代對(duì)象序列的新迭代器。新迭代器會(huì)無(wú)限重復(fù)這些元素。:

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

itertools.repeat(elem, [n]) 返回 n 次所提供的元素,當(dāng) n 不存在時(shí),返回?zé)o數(shù)次所提供的元素。

itertools.repeat('abc') =>
  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è)的所有元素,如此一直進(jìn)行下去,直到消耗掉所有輸入的可迭代對(duì)象。

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

itertools.islice(iter, [start], stop, [step]) 返回一個(gè)所輸入的迭代器切片的數(shù)據(jù)流。如果只單獨(dú)給定 stop 參數(shù)的話,它會(huì)返回從起始算起 stop 個(gè)數(shù)量的元素。如果你提供了起始下標(biāo) start,你會(huì)得到 stop-start 個(gè)元素;如果你給定了 step 參數(shù),數(shù)據(jù)流會(huì)跳過(guò)相應(yīng)的元素。和 Python 里的字符串和列表切片不同,你不能在 start, stop 或者 step 這些參數(shù)中使用負(fù)數(shù)。:

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è)能夠返回源迭代器內(nèi)容的獨(dú)立迭代器。如果你不提供參數(shù) n,默認(rèn)值為 2。復(fù)制迭代器需要保存源迭代器的一部分內(nèi)容,因此在源迭代器比較大的時(shí)候會(huì)顯著地占用內(nèi)存;同時(shí),在所有新迭代器中,有一個(gè)迭代器會(huì)比其他迭代器占用更多的內(nèi)存。

itertools.tee( itertools.count() ) =>
   iterA, iterB

where iterA ->
   0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...

and   iterB ->
   0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...

對(duì)元素使用函數(shù)?

operator 模塊包含一組對(duì)應(yīng)于 Python 操作符的函數(shù)。比如 operator.add(a, b) (把兩個(gè)數(shù)加起來(lái)),operator.ne(a, b) (和 a != b 相同),以及 operator.attrgetter('id') (返回獲取 .id 屬性的可調(diào)用對(duì)象)。

itertools.starmap(func, iter) 假定可迭代對(duì)象能夠返回一個(gè)元組的流,并且利用這些元組作為參數(shù)來(lái)調(diào)用 func:

itertools.starmap(os.path.join,
                  [('/bin', 'python'), ('/usr', 'bin', 'java'),
                   ('/usr', 'bin', 'perl'), ('/usr', 'bin', 'ruby')])
=>
  /bin/python, /usr/bin/java, /usr/bin/perl, /usr/bin/ruby

選擇元素?

另外一系列函數(shù)根據(jù)謂詞選取一個(gè)迭代器中元素的子集。

itertools.filterfalse(predicate, iter)filter() 相反,返回所有讓 predicate 返回 false 的元素:

itertools.filterfalse(is_even, itertools.count()) =>
  1, 3, 5, 7, 9, 11, 13, 15, ...

itertools.takewhile(predicate, iter) 返回一直讓 predicate 返回 true 的元素。一旦 predicate 返回 false,迭代器就會(huì)發(fā)出終止結(jié)果的信號(hào)。:

def less_than_10(x):
    return x < 10

itertools.takewhile(less_than_10, itertools.count()) =>
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9

itertools.takewhile(is_even, itertools.count()) =>
  0

itertools.dropwhile(predicate, iter) 在 predicate 返回 true 的時(shí)候丟棄元素,并且返回可迭代對(duì)象的剩余結(jié)果。:

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, ...

itertools.compress(data, selectors) 接受兩個(gè)迭代器,然后返回 data 中使相應(yīng)地 selector 中的元素為真的元素;它會(huì)在任一個(gè)迭代器耗盡的時(shí)候停止:

itertools.compress([1, 2, 3, 4, 5], [True, True, False, False, True]) =>
   1, 2, 5

組合函數(shù)?

itertools.combinations(iterable, r) 返回一個(gè)迭代器,它能給出輸入迭代器中所包含的元素的所有可能的 r 元元組的組合。:

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

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

每個(gè)元組中的元素保持著 可迭代對(duì)象 返回他們的順序。例如,在上面的例子中數(shù)字 1 總是會(huì)在 2, 3, 4 或 5 前面。一個(gè)類似的函數(shù),itertools.permutations(iterable, r=None),取消了保持順序的限制,返回所有可能的長(zhǎng)度為 r 的排列:

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

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

如果你不提供 r 參數(shù)的值,它會(huì)使用可迭代對(duì)象的長(zhǎng)度,也就是說(shuō)會(huì)排列所有的元素。

注意這些函數(shù)會(huì)輸出所有可能的位置組合,并不要求 可迭代對(duì)象 的內(nèi)容不重復(fù):

itertools.permutations('aba', 3) =>
  ('a', 'b', 'a'), ('a', 'a', 'b'), ('b', 'a', 'a'),
  ('b', 'a', 'a'), ('a', 'a', 'b'), ('a', 'b', 'a')

同一個(gè)元組 ('a', 'a', 'b') 出現(xiàn)了兩次,但是兩個(gè) 'a' 字符來(lái)自不同的位置。

itertools.combinations_with_replacement(iterable, r) 函數(shù)放松了一個(gè)不同的限制:元組中的元素可以重復(fù)。從概念講,為每個(gè)元組第一個(gè)位置選取一個(gè)元素,然后在選擇第二個(gè)元素前替換掉它。:

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

為元素分組?

我要討論的最后一個(gè)函數(shù),itertools.groupby(iter,key_func=None),是最復(fù)雜的函數(shù)。 key_func(elem) 是一個(gè)可以對(duì)迭代器返回的每個(gè)元素計(jì)算鍵值的函數(shù)。 如果你不提供這個(gè)鍵值函數(shù),它就會(huì)簡(jiǎn)化成每個(gè)元素自身。

groupby() 從所依據(jù)的可迭代對(duì)象中連續(xù)地收集具有相同值的元素,然后返回一個(gè)長(zhǎng)度為2的元組的數(shù)據(jù)流, 每個(gè)元組包含鍵值以及對(duì)應(yīng)這個(gè)鍵值的元素所組成的迭代器。

city_list = [('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL'),
             ('Anchorage', 'AK'), ('Nome', 'AK'),
             ('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ'),
             ...
            ]

def get_state(city_state):
    return city_state[1]

itertools.groupby(city_list, get_state) =>
  ('AL', iterator-1),
  ('AK', iterator-2),
  ('AZ', iterator-3), ...

where
iterator-1 =>
  ('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL')
iterator-2 =>
  ('Anchorage', 'AK'), ('Nome', 'AK')
iterator-3 =>
  ('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ')

groupby() 假定了所依據(jù)的可迭代對(duì)象的內(nèi)容已經(jīng)根據(jù)鍵值排序。注意,返回的迭代器也會(huì)使用所依據(jù)的可迭代對(duì)象,所以在請(qǐng)求迭代器 2和相應(yīng)的鍵之前你必須先消耗迭代器 1 的結(jié)果。

functools 模塊?

Python 2.5 中的 functools 模塊包含了一些高階函數(shù)。 高階函數(shù) 接受一個(gè)或多個(gè)函數(shù)作為輸入,返回新的函數(shù)。 這個(gè)模塊中最有用的工具是 functools.partial() 函數(shù)。

對(duì)于用函數(shù)式風(fēng)格編寫的程序,有時(shí)你會(huì)希望通過(guò)給定部分參數(shù),將已有的函數(shù)構(gòu)變形稱新的函數(shù)。考慮一個(gè) Python 函數(shù) f(a, b, c);你希望創(chuàng)建一個(gè)和 f(1, b, c) 等價(jià)的新函數(shù) g(b, c);也就是說(shuō)你給定了 f() 的一個(gè)參數(shù)的值。這就是所謂的“部分函數(shù)應(yīng)用”。

partial() 接受參數(shù) (function, arg1, arg2, ..., kwarg1=value1, kwarg2=value2)。它會(huì)返回一個(gè)可調(diào)用的對(duì)象,所以你能夠直接調(diào)用這個(gè)結(jié)果以使用給定參數(shù)的 function。

這里有一個(gè)很小但很現(xiàn)實(shí)的例子:

import functools

def log(message, subsystem):
    """Write the contents of 'message' to the specified subsystem."""
    print('%s: %s' % (subsystem, message))
    ...

server_log = functools.partial(log, subsystem='server')
server_log('Unable to open socket')

functools.reduce(func, iter, [initial_value]) 持續(xù)地在可迭代對(duì)象的所有元素上執(zhí)行操作,因此它不能夠用在無(wú)限的可迭代對(duì)象上。func 必須是一個(gè)接受兩個(gè)元素并返回一個(gè)值的函數(shù)。functools.reduce() 接受迭代器返回的前兩個(gè)元素 A 和 B 并計(jì)算 func(A, B) 。然后它會(huì)請(qǐng)求第三個(gè)元素,C,計(jì)算 func(func(A, B), C),然后把這個(gè)結(jié)果再和第四個(gè)元素組合并返回,如此繼續(xù)下去直到消耗整個(gè)可迭代對(duì)象。如果輸入的可迭代對(duì)象完全不返回任何值,TypeError 異常就會(huì)拋出。如果提供了初值(initial value),它會(huì)被用作起始值,也就是先計(jì)算 func(initial_value, A) 。:

>>>
>>> import operator, functools
>>> functools.reduce(operator.concat, ['A', 'BB', 'C'])
'ABBC'
>>> functools.reduce(operator.concat, [])
Traceback (most recent call last):
  ...
TypeError: reduce() of empty sequence with no initial value
>>> functools.reduce(operator.mul, [1, 2, 3], 1)
6
>>> functools.reduce(operator.mul, [], 1)
1

如果你在 functools.reduce() 中使用 operator.add(),你就會(huì)把可迭代對(duì)象中的所有元素加起來(lái).這種情況非常常見(jiàn), 所以 Python 有一個(gè)特殊的內(nèi)置函數(shù) sum():

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

不過(guò), 對(duì)于很多使用 functools.reduce() 的情形, 使用明顯的 for 循環(huán)會(huì)更清晰:

import functools
# Instead of:
product = functools.reduce(operator.mul, [1, 2, 3], 1)

# You can write:
product = 1
for i in [1, 2, 3]:
    product *= i

一個(gè)相關(guān)的函數(shù)是 itertools.accumulate(iterable, func=operator.add) 。它執(zhí)行同樣的計(jì)算, 不過(guò)相對(duì)于只返回最終結(jié)果,accumulate() 會(huì)返回一個(gè)迭代器來(lái)輸出所有中間結(jié)果:

itertools.accumulate([1, 2, 3, 4, 5]) =>
  1, 3, 6, 10, 15

itertools.accumulate([1, 2, 3, 4, 5], operator.mul) =>
  1, 2, 6, 24, 120

operator 模塊?

前面已經(jīng)提到了 operator 模塊。它包含一系列對(duì)應(yīng)于 Python 操作符的函數(shù)。在函數(shù)式風(fēng)格的代碼中,這些函數(shù)通常很有用,可以幫你省下不少時(shí)間,避免寫一些瑣碎的僅僅執(zhí)行一個(gè)簡(jiǎn)單操作的函數(shù)。

這個(gè)模塊里的一些函數(shù):

  • 數(shù)學(xué)運(yùn)算: add()sub(),mul()floordiv(),abs(), ...

  • 邏輯運(yùn)算: not_(),truth()

  • 位運(yùn)算: and_(),or_(),invert()。

  • 比較: eq()ne(),lt(),le(),gt(),和 ge()。

  • 確認(rèn)對(duì)象: is_(),is_not()

全部函數(shù)列表可以參考 operator 模塊的文檔。

小函數(shù)和 lambda 表達(dá)式?

編寫函數(shù)式風(fēng)格程序時(shí),你會(huì)經(jīng)常需要很小的函數(shù),作為謂詞函數(shù)或者以某種方式來(lái)組合元素。

如果合適的 Python 內(nèi)置的或者其他模塊中的函數(shù),你就一點(diǎn)也不需要定義新的函數(shù):

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

如果不存在你需要的函數(shù),你就必須自己編寫。一個(gè)編寫小函數(shù)的方式是使用 lambda 表達(dá)式。lambda 接受一組參數(shù)以及組合這些參數(shù)的表達(dá)式,它會(huì)創(chuàng)建一個(gè)返回表達(dá)式值的匿名函數(shù):

adder = lambda x, y: x+y

print_assign = lambda name, value: name + '=' + str(value)

另一種替代方案就是通常的使用 def 語(yǔ)句來(lái)定義函數(shù):

def adder(x, y):
    return x + y

def print_assign(name, value):
    return name + '=' + str(value)

哪一種更受青睞呢?這是一個(gè)風(fēng)格問(wèn)題;我通常的做法是避免使用 lambda。

我這么偏好的一個(gè)原因是,lambda 能夠定義的函數(shù)非常受限。函數(shù)的結(jié)果必須能夠作為單獨(dú)的表達(dá)式來(lái)計(jì)算,這意味著你不能使用多路 if... elif... else 比較,或者 try... except 語(yǔ)句。如果你嘗試在 lambda 語(yǔ)句中做太多事情,你最終會(huì)把表達(dá)式過(guò)于復(fù)雜以至于難以閱讀。你能快速的說(shuō)出下面的代碼做了什么事情嗎?:

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

你可以弄明白,不過(guò)要花上時(shí)間來(lái)理清表達(dá)式來(lái)搞清楚發(fā)生了什么。使用一個(gè)簡(jiǎn)短的嵌套的 def 語(yǔ)句可以讓情況變得更好:

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

total = functools.reduce(combine, items)[1]

如果我僅僅使用一個(gè) for 循環(huán)會(huì)更好:

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

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

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

許多使用 functools.reduce() 的情形可以更清晰地寫成 for 循環(huán)的形式。

Fredrik Lundh 曾經(jīng)建議以下一組規(guī)則來(lái)重構(gòu) lambda 的使用:

  1. 寫一個(gè) lambda 函數(shù)。

  2. 寫一句注釋來(lái)說(shuō)明這個(gè) lambda 究竟干了什么。

  3. 研究一會(huì)這個(gè)注釋,然后想出一個(gè)抓住注釋本質(zhì)的名字。

  4. 用這個(gè)名字,把這個(gè) lambda 改寫成 def 語(yǔ)句。

  5. 把注釋去掉。

我非常喜歡這些規(guī)則,不過(guò)你完全有權(quán)利爭(zhēng)辯這種消除 lambda 的風(fēng)格是不是更好。

修訂記錄和致謝?

作者要感謝以下人員對(duì)本文各種草稿給予的建議,更正和協(xié)助:Ian Bicking,Nick Coghlan, Nick Efford, Raymond Hettinger, Jim Jewett, Mike Krell,Leandro Lameiro, Jussi Salmela, Collin Winter, Blake Winton。

0.1 版: 2006 年 6 月 30 日發(fā)布。

0.11 版: 2006 年 7 月 1 日發(fā)布。 修正拼寫錯(cuò)誤。

0.2 版: 2006 年 7 月 10 日發(fā)布。 將 genexp 與 listcomp 兩節(jié)合二為一。 修正拼寫錯(cuò)誤。

0.21 版: 加入了 tutor 郵件列表中建議的更多參考文件。

0.30 版: 添加了有關(guān) functional 模塊的小節(jié),由 Collin Winter 撰寫;添加了有關(guān) operator 模塊的簡(jiǎn)短小節(jié);其他少量修改。

參考文獻(xiàn)?

通用文獻(xiàn)?

Structure and Interpretation of Computer Programs, Harold Abelson, Gerald Jay Sussman 和 Julie Sussman 著。全文可見(jiàn) https://mitpress.mit.edu/sicp/ 。在這部計(jì)算機(jī)科學(xué)的經(jīng)典教科書(shū)中,第二和第三章討論了使用序列和流來(lái)組織程序內(nèi)部的數(shù)據(jù)傳遞。書(shū)中的示例采用 Scheme 語(yǔ)言,但其中這些章節(jié)中描述的很多設(shè)計(jì)方法同樣適用于函數(shù)式風(fēng)格的 Python 代碼。

http://www.defmacro.org/ramblings/fp.html: 一個(gè)使用 Java 示例的函數(shù)式編程的總體介紹,有很長(zhǎng)的歷史說(shuō)明。

https://en.wikipedia.org/wiki/Functional_programming: 一般性的函數(shù)式編程的 Wikipedia 條目。

https://en.wikipedia.org/wiki/Coroutine: 協(xié)程條目。

https://en.wikipedia.org/wiki/Currying: 函數(shù)柯里化條目。

Python 相關(guān)?

http://gnosis.cx/TPiP/:David Mertz 書(shū)中的第一章 Text Processing in Python,"Utilizing Higher-Order Functions in Text Processing" 標(biāo)題部分討論了文本處理的函數(shù)式編程。

Mertz 還在 IBM 的 DeveloperWorks 站點(diǎn)上針對(duì)函數(shù)式編程撰寫了一系列共 3 篇文章;參見(jiàn) part 1, part 2part 3,

Python 文檔?

itertools 模塊文檔。

functools 模塊文檔。

operator 模塊文檔。

PEP 289: "Generator Expressions"

PEP 342: "Coroutines via Enhanced Generators" 描述了 Python 2.5 中新的生成器特性。