函數(shù)式編程指引?
- 作者
A. M. Kuchling
- 發(fā)布版本
0.32
本文檔提供恰當?shù)?Python 函數(shù)式編程范例,在函數(shù)式編程簡單的介紹之后,將簡單介紹Python中關于函數(shù)式編程的特性如 iterator 和 generator 以及相關庫模塊如 itertools
和 functools
等。
概述?
本章介紹函數(shù)式編程的基本概念。如您僅想學習 Python 語言的特性,可跳過本章直接查看 迭代器.
編程語言支持通過以下幾種方式來解構具體問題:
大多數(shù)的編程語言都是 過程式 的,所謂程序就是一連串告訴計算機怎樣處理程序輸入的指令。C、Pascal 甚至 Unix shells 都是過程式語言。
在 聲明式 語言中,你編寫一個用來描述待解決問題的說明,并且這個語言的具體實現(xiàn)會指明怎樣高效的進行計算。 SQL 可能是你最熟悉的聲明式語言了。 一個 SQL 查詢語句描述了你想要檢索的數(shù)據(jù)集,并且 SQL 引擎會決定是掃描整張表還是使用索引,應該先執(zhí)行哪些子句等等。
面向對象 程序會操作一組對象。 對象擁有內部狀態(tài),并能夠以某種方式支持請求和修改這個內部狀態(tài)的方法。Smalltalk 和 Java 都是面向對象的語言。 C++ 和 Python 支持面向對象編程,但并不強制使用面向對象特性。
函數(shù)式 編程則將一個問題分解成一系列函數(shù)。 理想情況下,函數(shù)只接受輸入并輸出結果,對一個給定的輸入也不會有影響輸出的內部狀態(tài)。 著名的函數(shù)式語言有 ML 家族(Standard ML,Ocaml 以及其他變種)和 Haskell。
一些語言的設計者選擇強調一種特定的編程方式。 這通常會讓以不同的方式來編寫程序變得困難。其他多范式語言則支持幾種不同的編程方式。Lisp,C++ 和 Python 都是多范式語言;使用這些語言,你可以編寫主要為過程式,面向對象或者函數(shù)式的程序和函數(shù)庫。在大型程序中,不同的部分可能會采用不同的方式編寫;比如 GUI 可能是面向對象的而處理邏輯則是過程式或者函數(shù)式。
在函數(shù)式程序里,輸入會流經(jīng)一系列函數(shù)。每個函數(shù)接受輸入并輸出結果。函數(shù)式風格反對使用帶有副作用的函數(shù),這些副作用會修改內部狀態(tài),或者引起一些無法體現(xiàn)在函數(shù)的返回值中的變化。完全不產(chǎn)生副作用的函數(shù)被稱作“純函數(shù)”。消除副作用意味著不能使用隨程序運行而更新的數(shù)據(jù)結構;每個函數(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ù)式風格的 Python 程序并不會極端到消除所有 I/O 或者賦值的程度;相反,他們會提供像函數(shù)式一樣的接口,但會在內部使用非函數(shù)式的特性。比如,函數(shù)的實現(xiàn)仍然會使用局部變量,但不會修改全局變量或者有其他副作用。
函數(shù)式編程可以被認為是面向對象編程的對立面。對象就像是顆小膠囊,包裹著內部狀態(tài)和隨之而來的能讓你修改這個內部狀態(tài)的一組調用方法,以及由正確的狀態(tài)變化所構成的程序。函數(shù)式編程希望盡可能地消除狀態(tài)變化,只和流經(jīng)函數(shù)的數(shù)據(jù)打交道。在 Python 里你可以把兩種編程方式結合起來,在你的應用(電子郵件信息,事務處理)中編寫接受和返回對象實例的函數(shù)。
函數(shù)式設計在工作中看起來是個奇怪的約束。為什么你要消除對象和副作用呢?不過函數(shù)式風格有其理論和實踐上的優(yōu)點:
形式證明。
模塊化。
組合性。
易于調試和測試。
形式證明?
一個理論上的優(yōu)點是,構造數(shù)學證明來說明函數(shù)式程序是正確的相對更容易些。
很長時間,研究者們對尋找證明程序正確的數(shù)學方法都很感興趣。這和通過大量輸入來測試,并得出程序的輸出基本正確,或者閱讀一個程序的源代碼然后得出代碼看起來沒問題不同;相反,這里的目標是一個嚴格的證明,證明程序對所有可能的輸入都能給出正確的結果。
證明程序正確性所用到的技術是寫出 不變量,也就是對于輸入數(shù)據(jù)和程序中的變量永遠為真的特性。然后對每行代碼,你說明這行代碼執(zhí)行前的不變量 X 和 Y 以及執(zhí)行后稍有不同的不變量 X' 和 Y' 為真。如此一直到程序結束,這時候在程序的輸出上,不變量應該會與期望的狀態(tài)一致。
函數(shù)式編程之所以要消除賦值,是因為賦值在這個技術中難以處理;賦值可能會破壞賦值前為真的不變量,卻并不產(chǎn)生任何可以傳遞下去的新的不變量。
不幸的是,證明程序的正確性很大程度上是經(jīng)驗性質的,而且和 Python 軟件無關。即使是微不足道的程序都需要幾頁長的證明;一個中等復雜的程序的正確性證明會非常龐大,而且,極少甚至沒有你日常所使用的程序(Python 解釋器,XML 解析器,瀏覽器)的正確性能夠被證明。即使你寫出或者生成一個證明,驗證證明也會是一個問題;里面可能出了差錯,而你錯誤地相信你證明了程序的正確性。
模塊化?
函數(shù)式編程的一個更實用的優(yōu)點是,它強制你把問題分解成小的方面。因此程序會更加模塊化。相對于一個進行了復雜變換的大型函數(shù),一個小的函數(shù)更明確,更易于編寫, 也更易于閱讀和檢查錯誤。
易于調試和測試?
測試和調試函數(shù)式程序相對來說更容易。
調試很簡單是因為函數(shù)通常都很小而且清晰明確。當程序無法工作的時候,每個函數(shù)都是一個可以檢查數(shù)據(jù)是否正確的接入點。你可以通過查看中間輸入和輸出迅速找到出錯的函數(shù)。
測試更容易是因為每個函數(shù)都是單元測試的潛在目標。在執(zhí)行測試前,函數(shù)并不依賴于需要重現(xiàn)的系統(tǒng)狀態(tài);相反,你只需要給出正確的輸入,然后檢查輸出是否和期望的結果一致。
組合性?
當你編寫函數(shù)式風格的程序時,你會寫出很多帶有不同輸入和輸出的函數(shù)。其中一些不可避免地會局限于特定的應用,但其他的卻可以廣泛的用在程序中。舉例來說,一個接受文件夾目錄返回所有文件夾中的 XML 文件的函數(shù); 或是一個接受文件名,然后返回文件內容的函數(shù),都可以應用在很多不同的場合。
久而久之你會形成一個個人工具庫。通常你可以重新組織已有的函數(shù)來組成新的程序,然后為當前的工作寫一些特殊的函數(shù)。
迭代器?
我會從 Python 的一個語言特性, 編寫函數(shù)式風格程序的重要基石開始說起:迭代器。
迭代器是一個表示數(shù)據(jù)流的對象;這個對象每次只返回一個元素。Python 迭代器必須支持 __next__()
方法;這個方法不接受參數(shù),并總是返回數(shù)據(jù)流中的下一個元素。如果數(shù)據(jù)流中沒有元素,__next__()
會拋出 StopIteration
異常。迭代器未必是有限的;完全有理由構造一個輸出無限數(shù)據(jù)流的迭代器。
內置的 iter()
函數(shù)接受任意對象并試圖返回一個迭代器來輸出對象的內容或元素,并會在對象不支持迭代的時候拋出 TypeError
異常。Python 有幾種內置數(shù)據(jù)類型支持迭代,最常見的就是列表和字典。如果一個對象能生成迭代器,那么它就會被稱作 iterable。
你可以手動試驗迭代器的接口。
>>> 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 有不少要求使用可迭代的對象的地方,其中最重要的就是 for
表達式。在表達式 for X in Y
,Y 要么自身是一個迭代器,要么能夠由 iter()
創(chuàng)建一個迭代器。以下兩種表達是等價的:
for i in iter(obj):
print(i)
for i in obj:
print(i)
可以用 list()
或 tuple()
這樣的構造函數(shù)把迭代器具體化成列表或元組:
>>> 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)
像 max()
和 min()
這樣的內置函數(shù)可以接受單個迭代器參數(shù),然后返回其中最大或者最小的元素。"in"
和 "not in"
操作也支持迭代器:如果能夠在迭代器 iterator 返回的數(shù)據(jù)流中找到 X 的話,則``X in iterator`` 為真。很顯然,如果迭代器是無限的,這么做你就會遇到問題;max()
和 min()
永遠也不會返回;如果元素 X 也不出現(xiàn)在數(shù)據(jù)流中,"in"
和 "not in"
操作同樣也永遠不會返回。
注意你只能在迭代器中順序前進;沒有獲取前一個元素的方法,除非重置迭代器,或者重新復制一份。迭代器對象可以提供這些額外的功能,但迭代器協(xié)議只明確了 __next__()
方法。函數(shù)可能因此而耗盡迭代器的輸出,如果你要對同樣的數(shù)據(jù)流做不同的操作,你必須重新創(chuàng)建一個迭代器。
支持迭代器的數(shù)據(jù)類型?
我們已經(jīng)知道列表和元組支持迭代器。實際上,Python 中的任何序列類型,比如字符串,都自動支持創(chuàng)建迭代器。
對字典調用 iter()
會返回一個遍歷字典的鍵的迭代器:
>>> 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 開始,字典的遍歷順序一定和輸入順序一樣。先前的版本并沒有明確這一點,所以不同的實現(xiàn)可能不一致。
對字典使用 iter()
總是會遍歷鍵,但字典也有返回其他迭代器的方法。如果你只遍歷值或者鍵/值對,你可以明確地調用 values()
或 items()
方法得到合適的迭代器。
dict()
構造函數(shù)可以接受一個迭代器,然后返回一個有限的 (key, value)
元組的數(shù)據(jù)流:
>>> L = [('Italy', 'Rome'), ('France', 'Paris'), ('US', 'Washington DC')]
>>> dict(iter(L))
{'Italy': 'Rome', 'France': 'Paris', 'US': 'Washington DC'}
文件也可以通過調用 readline()
來遍歷,直到窮盡文件中所有的行。這意味著你可以像這樣讀取文件中的每一行:
for line in file:
# do something for each line
...
集合可以從可遍歷的對象獲取內容,也可以讓你遍歷集合的元素:
S = {2, 3, 5, 7, 11, 13}
for i in S:
print(i)
生成器表達式和列表推導式?
迭代器的輸出有兩個很常見的使用方式,1) 對每一個元素執(zhí)行操作,2) 選擇一個符合條件的元素子集。比如,給定一個字符串列表,你可能想去掉每個字符串尾部的空白字符,或是選出所有包含給定子串的字符串。
列表推導式和生成器表達時(簡寫:"listcomps" 和 "genexps")讓這些操作更加簡明,這個形式借鑒自函數(shù)式程序語言 Haskell(https://www.haskell.org/)。你可以用以下代碼去掉一個字符串流中的所有空白字符:
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]
你可以加上條件語句 "if"
來選取特定的元素:
stripped_list = [line.strip() for line in line_list
if line != ""]
通過列表推導式,你會獲得一個 Python 列表;stripped_list
就是一個包含所有結果行的列表,并不是迭代器。 生成器表達式會返回一個迭代器,它在必要的時候計算結果,避免一次性生成所有的值。 這意味著,如果迭代器返回一個無限數(shù)據(jù)流或者大量的數(shù)據(jù),列表推導式就不太好用了。 這種情況下生成器表達式會更受青睞。
生成器表達式兩邊使用圓括號 ("()") ,而列表推導式則使用方括號 ("[]")。生成器表達式的形式為:
( 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 )
再次說明,列表推導式只有兩邊的括號不一樣(方括號而不是圓括號)。
這些生成用于輸出的元素會成為 expression
的后繼值。其中 if
語句是可選的;如果給定的話 expression
只會在符合條件時計算并加入到結果中。
生成器表達式總是寫在圓括號里面,不過也可以算上調用函數(shù)時用的括號。如果你想即時創(chuàng)建一個傳遞給函數(shù)的迭代器,可以這么寫:
obj_total = sum(obj.count for obj in list_all_objects())
其中 for...in
語句包含了將要遍歷的序列。這些序列并不必須同樣長,因為它們會從左往右開始遍歷,而 不是 同時執(zhí)行。對每個 sequence1
中的元素,sequence2
會從頭開始遍歷。sequence3
會對每個 sequence1
和 sequence2
的元素對開始遍歷。
換句話說,列表推導式器是和下面的 Python 代碼等價:
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.
這說明,如果有多個 for...in
語句而沒有 if
語句,輸出結果的長度就是所有序列長度的乘積。如果你的兩個列表長度為3,那么輸出的列表長度就是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
會生成元組,那這個元組必須要用括號括起來。下面第一個列表推導式語法錯誤,第二個則是正確的:
# Syntax error
[x, y for x in seq1 for y in seq2]
# Correct
[(x, y) for x in seq1 for y in seq2]
生成器?
生成器是一類用來簡化編寫迭代器工作的特殊函數(shù)。普通的函數(shù)計算并返回一個值,而生成器返回一個能返回數(shù)據(jù)流的迭代器。
毫無疑問,你已經(jīng)對如何在 Python 和 C 中調用普通函數(shù)很熟悉了,這時候函數(shù)會獲得一個創(chuàng)建局部變量的私有命名空間。當函數(shù)到達 return
表達式時,局部變量會被銷毀然后把返回給調用者。之后調用同樣的函數(shù)時會創(chuàng)建一個新的私有命名空間和一組全新的局部變量。但是,如果在退出一個函數(shù)時不扔掉局部變量會如何呢?如果稍后你能夠從退出函數(shù)的地方重新恢復又如何呢?這就是生成器所提供的;他們可以被看成可恢復的函數(shù)。
這里有簡單的生成器函數(shù)示例:
>>> def generate_ints(N):
... for i in range(N):
... yield i
任何包含了 yield
關鍵字的函數(shù)都是生成器函數(shù);Python 的 bytecode 編譯器會在編譯的時候檢測到并因此而特殊處理。
當你調用一個生成器函數(shù),它并不會返回單獨的值,而是返回一個支持生成器協(xié)議的生成器對象。當執(zhí)行 yield
表達式時,生成器會輸出 i
的值,就像 return
表達式一樣。yield
和 return
最大的區(qū)別在于,到達 yield
的時候生成器的執(zhí)行狀態(tài)會掛起并保留局部變量。在下一次調用生成器 __next__()
方法的時候,函數(shù)會恢復執(zhí)行。
這里有一個 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
會觸發(fā)從 __next__()
方法拋出 StopIteration(value)
異常。一旦拋出這個異常,或者函數(shù)結束,處理數(shù)據(jù)的過程就會停止,生成器也不會再生成新的值。
你可以手動編寫自己的類來達到生成器的效果,把生成器的所有局部變量作為實例的成員變量存儲起來。比如,可以這么返回一個整數(shù)列表:把 self.count
設為0,然后通過 count`()
。然而,對于一個中等復雜程度的生成器,寫出一個相應的類可能會相當繁雜。
包含在 Python 庫中的測試套件 Lib/test/test_generators.py 里有很多非常有趣的例子。這里是一個用生成器實現(xiàn)樹的遞歸中序遍歷示例。:
# 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
另外兩個 test_generators.py
中的例子給出了 N 皇后問題(在 NxN 的棋盤上放置 N 個皇后,任何一個都不能吃掉另一個),以及馬的遍歷路線(在NxN 的棋盤上給馬找出一條不重復的走過所有格子的路線)的解。
向生成器傳遞值?
在 Python 2.4 及之前的版本中,生成器只產(chǎn)生輸出。一旦調用生成器的代碼創(chuàng)建一個迭代器,就沒有辦法在函數(shù)恢復執(zhí)行的時候向它傳遞新的信息。你可以設法實現(xiàn)這個功能,讓生成器引用一個全局變量或者一個調用者可以修改的可變對象,但是這些方法都很繁雜。
在 Python 2.5 里有一個簡單的將值傳遞給生成器的方法。yield
變成了一個表達式,返回一個可以賦給變量或執(zhí)行操作的值:
val = (yield i)
我建議你在處理 yield
表達式返回值的時候, 總是 兩邊寫上括號,就像上面的例子一樣。括號并不總是必須的,但是比起記住什么時候需要括號,寫出來會更容易一點。
(PEP 342 解釋了具體的規(guī)則,也就是 yield
表達式必須括起來,除非是出現(xiàn)在最頂級的賦值表達式的右邊。這意味著你可以寫 val = yield i
,但是必須在操作的時候加上括號,就像``val = (yield i) + 12``)
可以調用 send(value)()
<generator.send> 方法向生成器發(fā)送值。這個方法會恢復執(zhí)行生成器的代碼,然后 yield
表達式返回特定的值。如果調用普通的 __next__`方法,``yield`()
會返回 None
.
這里有一個簡單的每次加1的計數(shù)器,并允許改變內部計數(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
這是改變計數(shù)器的一個示例
>>> 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
因為 yield
很多時候會返回 None
,所以你應該總是檢查這個情況。不要在表達式中使用 yield
的值,除非你確定 send()
是唯一的用來恢復你的生成器函數(shù)的方法。
除了 send()
之外,生成器還有兩個其他的方法:
throw(value)
is used to raise an exception inside the generator; the exception is raised by theyield
expression where the generator's execution is paused.generator.close()
會在生成器內部拋出GeneratorExit
異常來結束迭代。當接收到這個異常時,生成器的代碼會拋出GeneratorExit
或者StopIteration
;捕捉這個異常作其他處理是非法的,并會出發(fā)RuntimeError
。close()
也會在 Python 垃圾回收器回收生成器的時候調用。如果你要在
GeneratorExit
發(fā)生的時候清理代碼,我建議使用try: ... finally:
組合來代替GeneratorExit
。
這些改變的累積效應是,讓生成器從單向的信息生產(chǎn)者變成了既是生產(chǎn)者,又是消費者。
生成器也可以成為 協(xié)程 ,一種更廣義的子過程形式。子過程可以從一個地方進入,然后從另一個地方退出(從函數(shù)的頂端進入,從 return
語句退出),而協(xié)程可以進入,退出,然后在很多不同的地方恢復(yield
語句)。
內置函數(shù)?
我們可以看看迭代器常常用到的函數(shù)的更多細節(jié)。
Python 內置的兩個函數(shù) map()
和 filter()
復制了生成器表達式的兩個特性:
map(f, iterA, iterB, ...)
返回一個遍歷序列的迭代器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']
你當然也可以用列表推導式達到同樣的效果。
filter(predicate, iter)
返回一個遍歷序列中滿足指定條件的元素的迭代器,和列表推導式的功能相似。 predicate (謂詞)是一個在特定條件下返回真值的函數(shù);要使用函數(shù) filter()
,謂詞函數(shù)必須只能接受一個參數(shù)。
>>> def is_even(x):
... return (x % 2) == 0
>>> list(filter(is_even, range(10)))
[0, 2, 4, 6, 8]
這也可以寫成列表推導式:
>>> list(x for x in range(10) if is_even(x))
[0, 2, 4, 6, 8]
enumerate(iter, start=0)
計數(shù)可迭代對象中的元素,然后返回包含每個計數(shù)(從 start 開始)和元素兩個值的元組。:
>>> for item in enumerate(['subject', 'verb', 'object']):
... print(item)
(0, 'subject')
(1, 'verb')
(2, 'object')
enumerate()
常常用于遍歷列表并記錄達到特定條件時的下標:
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)
會將 iterable 中的元素收集到一個列表中,然后排序并返回結果。其中 key 和 reverse 參數(shù)會傳遞給所創(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]
(對排序更詳細的討論可參見 排序指南。)
內置函數(shù) any(iter)
和 all(iter)
會查看一個可迭代對象內容的邏輯值。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
zip(iterA, iterB, ...)
從每個可迭代對象中選取單個元素組成列表并返回:
zip(['a', 'b', 'c'], (1, 2, 3)) =>
('a', 1), ('b', 2), ('c', 3)
它并不會在內存創(chuàng)建一個列表并因此在返回前而耗盡輸入的迭代器;相反,只有在被請求的時候元組才會創(chuàng)建并返回。(這種行為的技術術語叫惰性計算,參見 lazy evaluation.)
這個迭代器設計用于長度相同的可迭代對象。如果可迭代對象的長度不一致,返回的數(shù)據(jù)流的長度會和最短的可迭代對象相同
zip(['a', 'b'], (1, 2, 3)) =>
('a', 1), ('b', 2)
然而,你應該避免這種情況,因為所有從更長的迭代器中取出的元素都會被丟棄。這意味著之后你也無法冒著跳過被丟棄元素的風險來繼續(xù)使用這個迭代器。
itertools 模塊?
itertools
模塊包含很多常用的迭代器以及用來組合迭代器的函數(shù)。本節(jié)會用些小的例子來介紹這個模塊的內容。
這個模塊里的函數(shù)大致可以分為幾類:
從已有的迭代器創(chuàng)建新的迭代器的函數(shù)。
接受迭代器元素作為參數(shù)的函數(shù)。
選取部分迭代器輸出的函數(shù)。
給迭代器輸出分組的函數(shù)。
創(chuàng)建新的迭代器?
itertools.count(start, step)
返回一個等分的無限數(shù)據(jù)流。初始值默認為0,間隔默認為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)
保存一份所提供的可迭代對象的副本,并返回一個能產(chǎn)生整個可迭代對象序列的新迭代器。新迭代器會無限重復這些元素。:
itertools.cycle([1, 2, 3, 4, 5]) =>
1, 2, 3, 4, 5, 1, 2, 3, 4, 5, ...
itertools.repeat(elem, [n])
返回 n 次所提供的元素,當 n 不存在時,返回無數(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ù)量的可迭代對象作為輸入,首先返回第一個迭代器的所有元素,然后是第二個的所有元素,如此一直進行下去,直到消耗掉所有輸入的可迭代對象。
itertools.chain(['a', 'b', 'c'], (1, 2, 3)) =>
a, b, c, 1, 2, 3
itertools.islice(iter, [start], stop, [step])
返回一個所輸入的迭代器切片的數(shù)據(jù)流。如果只單獨給定 stop 參數(shù)的話,它會返回從起始算起 stop 個數(shù)量的元素。如果你提供了起始下標 start,你會得到 stop-start 個元素;如果你給定了 step 參數(shù),數(shù)據(jù)流會跳過相應的元素。和 Python 里的字符串和列表切片不同,你不能在 start, stop 或者 step 這些參數(shù)中使用負數(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])
可以復制一個迭代器;它返回 n 個能夠返回源迭代器內容的獨立迭代器。如果你不提供參數(shù) n,默認值為 2。復制迭代器需要保存源迭代器的一部分內容,因此在源迭代器比較大的時候會顯著地占用內存;同時,在所有新迭代器中,有一個迭代器會比其他迭代器占用更多的內存。
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, ...
對元素使用函數(shù)?
operator
模塊包含一組對應于 Python 操作符的函數(shù)。比如 operator.add(a, b)
(把兩個數(shù)加起來),operator.ne(a, b)
(和 a != b
相同),以及 operator.attrgetter('id')
(返回獲取 .id
屬性的可調用對象)。
itertools.starmap(func, iter)
假定可迭代對象能夠返回一個元組的流,并且利用這些元組作為參數(shù)來調用 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ù)謂詞選取一個迭代器中元素的子集。
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,迭代器就會發(fā)出終止結果的信號。:
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 的時候丟棄元素,并且返回可迭代對象的剩余結果。:
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)
接受兩個迭代器,然后返回 data 中使相應地 selector 中的元素為真的元素;它會在任一個迭代器耗盡的時候停止:
itertools.compress([1, 2, 3, 4, 5], [True, True, False, False, True]) =>
1, 2, 5
組合函數(shù)?
itertools.combinations(iterable, r)
返回一個迭代器,它能給出輸入迭代器中所包含的元素的所有可能的 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)
每個元組中的元素保持著 可迭代對象 返回他們的順序。例如,在上面的例子中數(shù)字 1 總是會在 2, 3, 4 或 5 前面。一個類似的函數(shù),itertools.permutations(iterable, r=None)
,取消了保持順序的限制,返回所有可能的長度為 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ù)的值,它會使用可迭代對象的長度,也就是說會排列所有的元素。
注意這些函數(shù)會輸出所有可能的位置組合,并不要求 可迭代對象 的內容不重復:
itertools.permutations('aba', 3) =>
('a', 'b', 'a'), ('a', 'a', 'b'), ('b', 'a', 'a'),
('b', 'a', 'a'), ('a', 'a', 'b'), ('a', 'b', 'a')
同一個元組 ('a', 'a', 'b')
出現(xiàn)了兩次,但是兩個 'a' 字符來自不同的位置。
itertools.combinations_with_replacement(iterable, r)
函數(shù)放松了一個不同的限制:元組中的元素可以重復。從概念講,為每個元組第一個位置選取一個元素,然后在選擇第二個元素前替換掉它。:
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)
為元素分組?
我要討論的最后一個函數(shù),itertools.groupby(iter,key_func=None)
,是最復雜的函數(shù)。 key_func(elem)
是一個可以對迭代器返回的每個元素計算鍵值的函數(shù)。 如果你不提供這個鍵值函數(shù),它就會簡化成每個元素自身。
groupby()
從所依據(jù)的可迭代對象中連續(xù)地收集具有相同值的元素,然后返回一個長度為2的元組的數(shù)據(jù)流, 每個元組包含鍵值以及對應這個鍵值的元素所組成的迭代器。
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ù)的可迭代對象的內容已經(jīng)根據(jù)鍵值排序。注意,返回的迭代器也會使用所依據(jù)的可迭代對象,所以在請求迭代器 2和相應的鍵之前你必須先消耗迭代器 1 的結果。
functools 模塊?
Python 2.5 中的 functools
模塊包含了一些高階函數(shù)。 高階函數(shù) 接受一個或多個函數(shù)作為輸入,返回新的函數(shù)。 這個模塊中最有用的工具是 functools.partial()
函數(shù)。
對于用函數(shù)式風格編寫的程序,有時你會希望通過給定部分參數(shù),將已有的函數(shù)構變形稱新的函數(shù)??紤]一個 Python 函數(shù) f(a, b, c)
;你希望創(chuàng)建一個和 f(1, b, c)
等價的新函數(shù) g(b, c)
;也就是說你給定了 f()
的一個參數(shù)的值。這就是所謂的“部分函數(shù)應用”。
partial()
接受參數(shù) (function, arg1, arg2, ..., kwarg1=value1, kwarg2=value2)
。它會返回一個可調用的對象,所以你能夠直接調用這個結果以使用給定參數(shù)的 function
。
這里有一個很小但很現(xiàn)實的例子:
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ù)地在可迭代對象的所有元素上執(zhí)行操作,因此它不能夠用在無限的可迭代對象上。func 必須是一個接受兩個元素并返回一個值的函數(shù)。functools.reduce()
接受迭代器返回的前兩個元素 A 和 B 并計算 func(A, B)
。然后它會請求第三個元素,C,計算 func(func(A, B), C)
,然后把這個結果再和第四個元素組合并返回,如此繼續(xù)下去直到消耗整個可迭代對象。如果輸入的可迭代對象完全不返回任何值,TypeError
異常就會拋出。如果提供了初值(initial value),它會被用作起始值,也就是先計算 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()
,你就會把可迭代對象中的所有元素加起來.這種情況非常常見, 所以 Python 有一個特殊的內置函數(shù) sum()
:
>>> import functools, operator
>>> functools.reduce(operator.add, [1, 2, 3, 4], 0)
10
>>> sum([1, 2, 3, 4])
10
>>> sum([])
0
不過, 對于很多使用 functools.reduce()
的情形, 使用明顯的 for
循環(huán)會更清晰:
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
一個相關的函數(shù)是 itertools.accumulate(iterable, func=operator.add)
。它執(zhí)行同樣的計算, 不過相對于只返回最終結果,accumulate()
會返回一個迭代器來輸出所有中間結果:
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
模塊。它包含一系列對應于 Python 操作符的函數(shù)。在函數(shù)式風格的代碼中,這些函數(shù)通常很有用,可以幫你省下不少時間,避免寫一些瑣碎的僅僅執(zhí)行一個簡單操作的函數(shù)。
這個模塊里的一些函數(shù):
數(shù)學運算:
add()
,sub()
,mul()
,floordiv()
,abs()
, ...邏輯運算:
not_()
,truth()
。位運算:
and_()
,or_()
,invert()
。比較:
eq()
,ne()
,lt()
,le()
,gt()
,和ge()
。確認對象:
is_()
,is_not()
。
全部函數(shù)列表可以參考 operator 模塊的文檔。
小函數(shù)和 lambda 表達式?
編寫函數(shù)式風格程序時,你會經(jīng)常需要很小的函數(shù),作為謂詞函數(shù)或者以某種方式來組合元素。
如果合適的 Python 內置的或者其他模塊中的函數(shù),你就一點也不需要定義新的函數(shù):
stripped_lines = [line.strip() for line in lines]
existing_files = filter(os.path.exists, file_list)
如果不存在你需要的函數(shù),你就必須自己編寫。一個編寫小函數(shù)的方式是使用 lambda
表達式。lambda
接受一組參數(shù)以及組合這些參數(shù)的表達式,它會創(chuàng)建一個返回表達式值的匿名函數(shù):
adder = lambda x, y: x+y
print_assign = lambda name, value: name + '=' + str(value)
另一種替代方案就是通常的使用 def
語句來定義函數(shù):
def adder(x, y):
return x + y
def print_assign(name, value):
return name + '=' + str(value)
哪一種更受青睞呢?這是一個風格問題;我通常的做法是避免使用 lambda
。
我這么偏好的一個原因是,lambda
能夠定義的函數(shù)非常受限。函數(shù)的結果必須能夠作為單獨的表達式來計算,這意味著你不能使用多路 if... elif... else
比較,或者 try... except
語句。如果你嘗試在 lambda
語句中做太多事情,你最終會把表達式過于復雜以至于難以閱讀。你能快速的說出下面的代碼做了什么事情嗎?:
import functools
total = functools.reduce(lambda a, b: (0, a[1] + b[1]), items)[1]
你可以弄明白,不過要花上時間來理清表達式來搞清楚發(fā)生了什么。使用一個簡短的嵌套的 def
語句可以讓情況變得更好:
import functools
def combine(a, b):
return 0, a[1] + b[1]
total = functools.reduce(combine, items)[1]
如果我僅僅使用一個 for
循環(huán)會更好:
total = 0
for a, b in items:
total += b
或者使用內置的 sum()
和一個生成器表達式:
total = sum(b for a, b in items)
許多使用 functools.reduce()
的情形可以更清晰地寫成 for
循環(huán)的形式。
Fredrik Lundh 曾經(jīng)建議以下一組規(guī)則來重構 lambda
的使用:
寫一個 lambda 函數(shù)。
寫一句注釋來說明這個 lambda 究竟干了什么。
研究一會這個注釋,然后想出一個抓住注釋本質的名字。
用這個名字,把這個 lambda 改寫成 def 語句。
把注釋去掉。
我非常喜歡這些規(guī)則,不過你完全有權利爭辯這種消除 lambda 的風格是不是更好。
修訂記錄和致謝?
作者要感謝以下人員對本文各種草稿給予的建議,更正和協(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ā)布。 修正拼寫錯誤。
0.2 版: 2006 年 7 月 10 日發(fā)布。 將 genexp 與 listcomp 兩節(jié)合二為一。 修正拼寫錯誤。
0.21 版: 加入了 tutor 郵件列表中建議的更多參考文件。
0.30 版: 添加了有關 functional
模塊的小節(jié),由 Collin Winter 撰寫;添加了有關 operator 模塊的簡短小節(jié);其他少量修改。
參考文獻?
通用文獻?
Structure and Interpretation of Computer Programs, Harold Abelson, Gerald Jay Sussman 和 Julie Sussman 著。全文可見 https://mitpress.mit.edu/sicp/ 。在這部計算機科學的經(jīng)典教科書中,第二和第三章討論了使用序列和流來組織程序內部的數(shù)據(jù)傳遞。書中的示例采用 Scheme 語言,但其中這些章節(jié)中描述的很多設計方法同樣適用于函數(shù)式風格的 Python 代碼。
http://www.defmacro.org/ramblings/fp.html: 一個使用 Java 示例的函數(shù)式編程的總體介紹,有很長的歷史說明。
https://en.wikipedia.org/wiki/Functional_programming: 一般性的函數(shù)式編程的 Wikipedia 條目。
https://en.wikipedia.org/wiki/Coroutine: 協(xié)程條目。
https://en.wikipedia.org/wiki/Currying: 函數(shù)柯里化條目。
Python 相關?
http://gnosis.cx/TPiP/:David Mertz 書中的第一章 Text Processing in Python,"Utilizing Higher-Order Functions in Text Processing" 標題部分討論了文本處理的函數(shù)式編程。
Mertz 還在 IBM 的 DeveloperWorks 站點上針對函數(shù)式編程撰寫了一系列共 3 篇文章;參見 part 1, part 2 和 part 3,
Python 文檔?
itertools
模塊文檔。
functools
模塊文檔。
operator
模塊文檔。
PEP 289: "Generator Expressions"
PEP 342: "Coroutines via Enhanced Generators" 描述了 Python 2.5 中新的生成器特性。