函數語言程式設計 HOWTO¶
- 作者:
A. M. Kuchling
- 釋出:
0.32
本文件將介紹 Python 中適合以函式式風格實現程式的功能。在介紹函數語言程式設計概念之後,我們將探討語言功能,如迭代器和生成器,以及相關庫模組,如itertools
和functools
。
引言¶
本節解釋了函數語言程式設計的基本概念;如果您只對學習 Python 語言特性感興趣,請跳到下一節迭代器。
程式語言以幾種不同的方式支援分解問題
大多數程式語言是 **過程式** 的:程式是指令列表,告訴計算機如何處理程式的輸入。C、Pascal,甚至 Unix shell 都是過程式語言。
在 **宣告式** 語言中,您編寫一個描述要解決問題的規範,語言實現會找出如何高效地執行計算。SQL 是您最可能熟悉的宣告式語言;SQL 查詢描述了您想要檢索的資料集,SQL 引擎決定是掃描表還是使用索引,哪些子句應該首先執行等。
**面向物件** 程式操作物件的集合。物件具有內部狀態並支援查詢或以某種方式修改此內部狀態的方法。Smalltalk 和 Java 是面嚮物件語言。C++ 和 Python 是支援面向物件程式設計但不會強制使用面向物件特性的語言。
**函式式** 程式設計將問題分解為一組函式。理想情況下,函式只接受輸入併產生輸出,並且沒有影響給定輸入所產生的輸出的任何內部狀態。著名的函式式語言包括 ML 家族(Standard ML、OCaml 和其他變體)和 Haskell。
一些計算機語言的設計者選擇強調一種特定的程式設計方法。這通常使得編寫使用不同方法的程式變得困難。其他語言是支援幾種不同方法的多正規化語言。Lisp、C++ 和 Python 是多正規化語言;您可以在所有這些語言中編寫主要面向過程、面向物件或函式式的程式或庫。在一個大型程式中,不同的部分可能使用不同的方法編寫;例如,GUI 可能是面向物件的,而處理邏輯是過程式或函式式的。
在函式式程式中,輸入流經一組函式。每個函式對其輸入進行操作併產生一些輸出。函式式風格不鼓勵帶有副作用的函式,這些副作用會修改內部狀態或進行函式返回值中不可見的更改。根本沒有副作用的函式稱為 **純函式**。避免副作用意味著不使用隨程式執行而更新的資料結構;每個函式的輸出必須只依賴於其輸入。
有些語言對純度要求非常嚴格,甚至沒有賦值語句,例如 a=3
或 c = a + b
,但很難避免所有副作用,例如列印到螢幕或寫入磁碟檔案。另一個例子是對 print()
或 time.sleep()
函式的呼叫,這兩個函式都不會返回有用的值。它們都只因將文字傳送到螢幕或暫停執行一秒的副作用而被呼叫。
以函式式風格編寫的 Python 程式通常不會走極端,避免所有 I/O 或所有賦值;相反,它們會提供一個函式式介面,但在內部會使用非函式式特性。例如,函式的實現仍會使用對區域性變數的賦值,但不會修改全域性變數或產生其他副作用。
函數語言程式設計可以被認為是面向物件程式設計的對立面。物件是包含一些內部狀態以及一系列允許您修改此狀態的方法呼叫的微型封裝,程式由進行正確的狀態更改組成。函數語言程式設計希望儘可能避免狀態更改,並處理函式之間流動的資料。在 Python 中,您可以透過編寫接受並返回表示應用程式中物件(電子郵件、事務等)例項的函式來結合這兩種方法。
函式式設計可能看起來是一個奇怪的限制。為什麼應該避免物件和副作用?函式式風格有理論和實踐上的優勢
形式可證性。
模組化。
可組合性。
易於除錯和測試。
形式可證性¶
一個理論上的好處是,構造一個函式式程式正確的數學證明更容易。
長期以來,研究人員一直對尋找數學上證明程式正確的方法感興趣。這不同於在大量輸入上測試程式並得出其輸出通常正確的結論,或者閱讀程式的原始碼並得出程式碼看起來正確的結論;目標是嚴格證明程式對所有可能的輸入都產生正確的結果。
用於證明程式正確的技術是寫下 **不變數**,即輸入資料和程式變數的始終為真的屬性。對於每一行程式碼,您然後證明如果 X 和 Y 不變數在執行該行 **之前** 為真,則略有不同的 X' 和 Y' 不變數在執行該行 **之後** 為真。這會一直持續到程式結束,此時不變數應與程式輸出的所需條件匹配。
函數語言程式設計避免賦值是因為賦值很難用這種技術處理;賦值可以在賦值之前打破為真的不變數,而不會產生任何可以繼續傳播的新不變數。
不幸的是,證明程式正確在很大程度上不切實際,並且與 Python 軟體無關。即使是微不足道的程式也需要數頁的證明;一箇中等複雜程式的正確性證明將是巨大的,您日常使用的程式(Python 直譯器、XML 解析器、Web 瀏覽器)中幾乎沒有或沒有程式可以被證明是正確的。即使您寫下或生成了一個證明,也會出現驗證證明的問題;也許其中存在錯誤,您錯誤地認為您已經證明了程式是正確的。
模組化¶
函數語言程式設計的一個更實際的好處是,它迫使你將問題分解成小塊。因此,程式更具模組化。指定和編寫一個執行一件事的小函式比執行復雜轉換的大函式更容易。小函式也更容易閱讀和檢查錯誤。
易於除錯和測試¶
測試和除錯函式式風格的程式更容易。
除錯變得簡單,因為函式通常很小且定義清晰。當程式不工作時,每個函式都是一個介面點,您可以在其中檢查資料是否正確。您可以檢視中間輸入和輸出,以快速隔離導致錯誤的函式。
測試更容易,因為每個函式都是單元測試的潛在主題。函式不依賴於需要在執行測試之前複製的系統狀態;相反,您只需合成正確的輸入,然後檢查輸出是否符合預期。
可組合性¶
在編寫函式式風格的程式時,您會編寫許多具有不同輸入和輸出的函式。其中一些函式將不可避免地專門用於特定應用程式,但其他函式將在各種程式中派上用場。例如,一個接收目錄路徑並返回目錄中所有 XML 檔案的函式,或者一個接收檔名並返回其內容的函式,可以應用於許多不同的情況。
隨著時間的推移,您將形成一個個人工具庫。通常,您將透過以新的配置排列現有函式併為當前任務編寫一些專門函式來組裝新程式。
迭代器¶
我將首先介紹 Python 的一個語言特性,它是編寫函式式風格程式的重要基礎:迭代器。
迭代器是一個表示資料流的物件;此物件一次返回一個元素。Python 迭代器必須支援一個名為 __next__()
的方法,該方法不帶任何引數並始終返回流中的下一個元素。如果流中沒有更多元素,__next__()
必須引發 StopIteration
異常。迭代器不必是有限的;編寫一個生成無限資料流的迭代器是完全合理的。
內建函式 iter()
接受一個任意物件,並嘗試返回一個將返回物件內容或元素的迭代器,如果物件不支援迭代,則會引發 TypeError
。Python 的幾個內建資料型別支援迭代,最常見的是列表和字典。如果可以為其獲取迭代器,則稱物件為 可迭代物件。
您可以手動嘗試迭代介面
>>> 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()
可以建立迭代器的物件。以下兩個語句是等效的
for i in iter(obj):
print(i)
for i in obj:
print(i)
迭代器可以使用 list()
或 tuple()
建構函式具體化為列表或元組
>>> 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()
可以接受一個迭代器引數,並將返回最大或最小元素。 "in"
和 "not in"
運算子也支援迭代器:如果迭代器返回的流中找到 X,則 X in iterator
為真。如果迭代器是無限的,您將遇到明顯的問題;max()
、min()
將永不返回,如果元素 X 從未出現在流中, "in"
和 "not in"
運算子也不會返回。
請注意,您只能在迭代器中前進;無法獲取上一個元素、重置迭代器或複製它。迭代器物件可以可選地提供這些附加功能,但迭代器協議只指定了 __next__()
方法。因此,函式可能會消耗迭代器的所有輸出,如果您需要對同一流進行不同的操作,則必須建立一個新的迭代器。
支援迭代器的資料型別¶
我們已經看到了列表和元組如何支援迭代器。實際上,任何 Python 序列型別,例如字串,都會自動支援迭代器的建立。
對字典呼叫 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 開始,字典的迭代順序保證與插入順序相同。在早期版本中,行為未指定,並且可能因實現而異。
對字典應用 iter()
總是迴圈遍歷鍵,但字典有返回其他迭代器的方法。如果您想遍歷值或鍵/值對,您可以顯式呼叫 values()
或 items()
方法以獲取適當的迭代器。
dict()
建構函式可以接受一個返回有限 (key, value)
元組流的迭代器
>>> 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)
2
3
5
7
11
13
生成器表示式和列表推導式¶
迭代器輸出的兩個常見操作是:1) 對每個元素執行某些操作,2) 選擇滿足某些條件的元素子集。例如,給定一個字串列表,您可能希望從每行中去除尾隨空格或提取所有包含給定子字串的字串。
列表推導式和生成器表示式(簡稱:“列表推導式”和“生成器表示式”)是用於此類操作的簡潔表示法,借鑑自函數語言程式設計語言 Haskell (https://www.haskell.org/)。您可以使用以下程式碼從字串流中去除所有空白字元
>>> line_list = [' line 1\n', 'line 2 \n', ' \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
是一個包含結果行的列表,而不是迭代器。生成器表示式返回一個按需計算值的迭代器,無需一次性具象化所有值。這意味著如果您正在處理返回無限流或大量資料的迭代器,列表推導式將沒有用。在這些情況下,生成器表示式更可取。
生成器表示式用括號 (“()”) 包裹,列表推導式用方括號 (“[]”) 包裹。生成器表示式的形式為
( 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
僅在 condition
為真時才會被求值並新增到結果中。
生成器表示式必須始終寫在括號內,但表示函式呼叫的括號也算數。如果您想建立一個迭代器並立即將其傳遞給函式,您可以這樣寫
obj_total = sum(obj.count for obj in list_all_objects())
for...in
子句包含要迭代的序列。序列的長度不必相同,因為它們是從左到右迭代的,**而不是** 並行迭代。對於 sequence1
中的每個元素,sequence2
從頭開始迴圈。然後,對於 sequence1
和 sequence2
中每個結果元素對,sequence3
會迴圈遍歷。
換句話說,列表推導式或生成器表示式等價於以下 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]
生成器¶
生成器是一類特殊的函式,它簡化了編寫迭代器的任務。普通函式計算一個值並返回它,但生成器返回一個迭代器,該迭代器返回一個值流。
您無疑熟悉 Python 或 C 中普通函式呼叫的工作方式。當您呼叫一個函式時,它會獲得一個私有名稱空間,其中建立了它的區域性變數。當函式到達 return
語句時,區域性變數被銷燬,值返回給呼叫者。稍後對同一函式的呼叫會建立一個新的私有名稱空間和一組新的區域性變數。但是,如果區域性變數在函式退出時沒有被丟棄怎麼辦?如果您以後可以在函式停止的地方恢復它怎麼辦?這就是生成器提供的;它們可以被認為是可恢復的函式。
這是一個生成器函式最簡單的例子:
>>> def generate_ints(N):
... for i in range(N):
... yield i
任何包含 yield
關鍵字的函式都是生成器函式;這由 Python 的 位元組碼 編譯器檢測到,因此會特殊編譯該函式。
當您呼叫生成器函式時,它不返回單個值;相反,它返回一個支援迭代器協議的生成器物件。執行 yield
表示式時,生成器輸出 i
的值,類似於 return
語句。yield
和 return
語句之間的巨大區別在於,當到達 yield
時,生成器的執行狀態被掛起,區域性變數被保留。下次呼叫生成器的 __next__()
方法時,函式將恢復執行。
以下是 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)
。
在生成器函式內部,return value
會導致從 __next__()
方法中引發 StopIteration(value)
。一旦發生這種情況,或者函式到達底部,值的處理就結束了,生成器無法再產生任何值。
你可以透過編寫自己的類並將生成器的所有區域性變數作為例項變數儲存來手動實現生成器的效果。例如,可以透過將 self.count
設定為 0,並讓 __next__()
方法遞增 self.count
並返回它來實現返回整數列表。然而,對於一箇中等複雜的生成器,編寫相應的類可能會更加混亂。
Python 庫中包含的測試套件 Lib/test/test_generators.py 包含許多更有趣的示例。這是一個使用生成器遞迴實現樹的中序遍歷的生成器。
# 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 及更早版本中,生成器只產生輸出。一旦生成器的程式碼被呼叫以建立迭代器,就沒有辦法在執行恢復時將任何新資訊傳遞給函式。您可以透過讓生成器檢視全域性變數或傳遞一些呼叫者隨後修改的可變物件來拼湊出這種能力,但這些方法都很混亂。
在 Python 2.5 中,有一種簡單的方法可以將值傳遞給生成器。yield
變成了一個表示式,返回一個可以賦值給變數或進行其他操作的值
val = (yield i)
當您對返回值進行操作時,我建議您 **始終** 在 yield
表示式周圍加上括號,如上面的示例所示。括號並非總是必需的,但始終新增它們比記住何時需要它們更容易。
(PEP 342 解釋了確切的規則,即 yield
表示式必須始終用括號括起來,除非它出現在賦值語句右側的頂級表示式中。這意味著您可以編寫 val = yield i
,但當有操作時必須使用括號,例如 val = (yield i) + 12
。)
透過呼叫生成器的 send(value)
方法將值傳送到生成器。此方法恢復生成器的程式碼,yield
表示式返回指定的值。如果呼叫常規的 __next__()
方法,yield
返回 None
。
這是一個簡單的計數器,它以 1 遞增並允許更改內部計數器的值。
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
下面是更改計數器的示例
>>> 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
,所以您應該始終檢查這種情況。除非您確定 send()
方法將是唯一用於恢復生成器函式的方法,否則不要在表示式中直接使用其值。
除了 send()
,生成器還有另外兩個方法
throw(value)
用於在生成器內部引發異常;異常由生成器執行暫停的yield
表示式引發。close()
向生成器傳送GeneratorExit
異常以終止迭代。收到此異常後,生成器的程式碼必須引發GeneratorExit
或StopIteration
;捕獲異常並執行其他任何操作都是非法的,並將觸發RuntimeError
。close()
也將在生成器被垃圾回收時由 Python 的垃圾回收器呼叫。如果需要在發生
GeneratorExit
時執行清理程式碼,我建議使用try: ... finally:
套件,而不是捕獲GeneratorExit
。
這些變化的累積效應是將生成器從單向的資訊生產者轉變為生產者和消費者。
生成器也成為 **協程**,一種更通用的子例程形式。子例程在一個點進入,在另一個點退出(函式頂部和 return
語句),但協程可以在許多不同點(yield
語句)進入、退出和恢復。
內建函式¶
讓我們更詳細地瞭解常與迭代器一起使用的內建函式。
Python 的兩個內建函式 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)
返回一個迭代器,遍歷所有滿足特定條件的序列元素,並且同樣可以透過列表推導式複製。 **謂詞** 是一個返回某個條件真值的函式;與 filter()
一起使用時,謂詞必須接受單個值。
>>> 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)
對可迭代物件中的元素進行計數,返回包含計數(從 *start* 開始)和每個元素的 2 元組。
>>> 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)
將可迭代物件的所有元素收集到一個列表中,對列表進行排序,並返回排序結果。 key 和 reverse 引數會傳遞給構建的列表的 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]
(有關排序的更詳細討論,請參閱 排序技巧。)
內建函式 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)
它不會在返回之前構造記憶體中的列表並耗盡所有輸入迭代器;相反,元組僅在請求時才構造並返回。(這種行為的技術術語是 惰性求值。)
此迭代器旨在與長度相同的所有可迭代物件一起使用。如果可迭代物件的長度不同,則結果流的長度將與最短的可迭代物件相同。
zip(['a', 'b'], (1, 2, 3)) =>
('a', 1), ('b', 2)
但是,您應該避免這樣做,因為元素可能會從較長的迭代器中取出並丟棄。這意味著您不能繼續使用迭代器,因為您可能會跳過已丟棄的元素。
itertools 模組¶
itertools
模組包含許多常用的迭代器以及用於組合多個迭代器的函式。本節將透過小例子介紹模組的內容。
模組的函式分為幾個大類
基於現有迭代器建立新迭代器的函式。
將迭代器元素作為函式引數處理的函式。
用於選擇迭代器輸出部分的函式。
用於分組迭代器輸出的函式。
建立新的迭代器¶
itertools.count(start, step)
返回一個均勻間隔的無限值流。您可以選擇提供起始數字(預設為 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)
儲存所提供可迭代物件內容的副本,並返回一個新迭代器,該迭代器按從頭到尾的順序返回其元素。新迭代器將無限重複這些元素。
itertools.cycle([1, 2, 3, 4, 5]) =>
1, 2, 3, 4, 5, 1, 2, 3, 4, 5, ...
itertools.repeat(elem, [n])
返回提供的元素 *n* 次,如果未提供 *n*,則無限次返回該元素。
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, ...)
接受任意數量的可迭代物件作為輸入,並返回第一個迭代器的所有元素,然後是第二個迭代器的所有元素,依此類推,直到所有可迭代物件都被耗盡。
itertools.chain(['a', 'b', 'c'], (1, 2, 3)) =>
a, b, c, 1, 2, 3
itertools.islice(iter, [start], stop, [step])
返回迭代器的一個切片流。如果只有一個 *stop* 引數,它將返回前 *stop* 個元素。如果您提供起始索引,您將得到 *stop-start* 個元素,如果您提供 *step* 的值,元素將相應跳過。與 Python 的字串和列表切片不同,您不能對 *start*、*stop* 或 *step* 使用負值。
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* 個獨立的迭代器,它們都將返回源迭代器的內容。如果您不提供 *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, ...
對元素呼叫函式¶
operator
模組包含一組對應於 Python 運算子的函式。一些例子包括 operator.add(a, b)
(將兩個值相加)、 operator.ne(a, b)
(與 a != b
相同) 和 operator.attrgetter('id')
(返回一個可呼叫物件,它獲取 .id
屬性)。
itertools.starmap(func, iter)
假定可迭代物件將返回元組流,並使用這些元組作為引數呼叫 *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
選擇元素¶
另一組函式根據謂詞選擇迭代器元素的子集。
itertools.filterfalse(predicate, iter)
與 filter()
相反,返回謂詞返回 false 的所有元素
itertools.filterfalse(is_even, itertools.count()) =>
1, 3, 5, 7, 9, 11, 13, 15, ...
itertools.takewhile(predicate, iter)
只要謂詞返回 true 就返回元素。一旦謂詞返回 false,迭代器將表示其結果的結束。
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)
在謂詞返回 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* 中對應 *selectors* 元素為 true 的那些元素,當其中一個耗盡時停止
itertools.compress([1, 2, 3, 4, 5], [True, True, False, False, True]) =>
1, 2, 5
組合函式¶
itertools.combinations(iterable, r)
返回一個迭代器,給出 *iterable* 中包含的元素的所有可能的 *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)
每個元組中的元素保持與 *iterable* 返回它們的順序相同。例如,在上面的示例中,數字 1 始終在 2、3、4 或 5 之前。一個類似的函式,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* 的值,則使用可迭代物件的長度,這意味著所有元素都將被排列。
請注意,這些函式透過位置生成所有可能的組合,並且不要求 *iterable* 的內容是唯一的
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')
出現了兩次,但兩個 'a' 字串來自不同的位置。
itertools.combinations_with_replacement(iterable, r)
函式放鬆了一個不同的約束:元素可以在單個元組中重複。從概念上講,為每個元組的第一個位置選擇一個元素,然後在選擇第二個元素之前替換該元素。
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)
分組元素¶
我將討論的最後一個函式 itertools.groupby(iter, key_func=None)
是最複雜的。key_func(elem)
是一個函式,可以為迭代器返回的每個元素計算一個鍵值。如果您不提供鍵函式,則鍵就是每個元素本身。
groupby()
收集底層可迭代物件中所有具有相同鍵值的連續元素,並返回一個包含鍵值和具有該鍵的元素的迭代器的 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 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()
假設底層可迭代物件的內容已經根據鍵進行了排序。請注意,返回的迭代器也使用底層可迭代物件,因此您必須在請求迭代器-2 及其相應鍵之前消耗迭代器-1 的結果。
functools 模組¶
functools
模組包含一些高階函式。 **高階函式** 接受一個或多個函式作為輸入並返回一個新函式。此模組中最有用的工具是 functools.partial()
函式。
對於以函式式風格編寫的程式,您有時會希望構造現有函式的變體,其中一些引數已填充。考慮一個 Python 函式 f(a, b, c)
;您可能希望建立一個新函式 g(b, c)
,它等價於 f(1, b, c)
;您正在為 f()
的一個引數填充值。這稱為“部分函式應用”。
partial()
的建構函式接受引數 (function, arg1, arg2, ..., kwarg1=value1, kwarg2=value2)
。結果物件是可呼叫的,因此您只需呼叫它即可使用已填充的引數呼叫 function
。
這是一個小而真實的例子
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])
對所有可迭代物件的元素累積執行操作,因此不能應用於無限可迭代物件。 *func* 必須是一個接受兩個元素並返回單個值的函式。functools.reduce()
接受迭代器返回的前兩個元素 A 和 B,並計算 func(A, B)
。然後它請求第三個元素 C,計算 func(func(A, B), C)
,將此結果與返回的第四個元素結合,並繼續直到可迭代物件耗盡。如果可迭代物件根本不返回任何值,則會引發 TypeError
異常。如果提供了初始值,它將用作起點,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
如果您將 operator.add()
與 functools.reduce()
結合使用,您將把可迭代物件的所有元素相加。這種情況非常常見,以至於有一個特殊的內建函式 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
迴圈會更清晰
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
一個相關的函式是 itertools.accumulate(iterable, func=operator.add)
。它執行相同的計算,但不是隻返回最終結果,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 模組¶
前面提到了 operator
模組。它包含一組與 Python 運算子對應的函式。這些函式在函式式風格的程式碼中通常很有用,因為它們可以避免您編寫執行單個操作的瑣碎函式。
此模組中的一些函式包括
數學運算:
add()
,sub()
,mul()
,floordiv()
,abs()
, …邏輯運算:
not_()
,truth()
。位運算:
and_()
,or_()
,invert()
。比較:
eq()
,ne()
,lt()
,le()
,gt()
和ge()
。物件標識:
is_()
,is_not()
。
查閱 operator 模組的文件以獲取完整列表。
小型函式和 lambda 表示式¶
在編寫函式式風格的程式時,您通常需要一些小的函式來充當謂詞或以某種方式組合元素。
如果有合適的 Python 內建函式或模組函式,您根本不需要定義新函式
stripped_lines = [line.strip() for line in lines]
existing_files = filter(os.path.exists, file_list)
如果你需要的函式不存在,你需要自己編寫。編寫小函式的一種方法是使用 lambda
表示式。lambda
接受一些引數和一個組合這些引數的表示式,並建立一個返回表示式值的匿名函式
adder = lambda x, y: x+y
print_assign = lambda name, value: name + '=' + str(value)
另一種方法是直接使用 def
語句,以通常的方式定義函式
def adder(x, y):
return x + y
def print_assign(name, value):
return name + '=' + str(value)
哪種替代方案更優選?這是一個風格問題;我通常的做法是避免使用 lambda
。
我偏愛的一個原因是 lambda
在其能定義的函式方面相當有限。結果必須可以計算為單個表示式,這意味著您不能有多路 if... elif... else
比較或 try... except
語句。如果您試圖在 lambda
語句中做太多事情,您最終會得到一個過於複雜的表示式,難以閱讀。快,下面的程式碼在做什麼?
import functools
total = functools.reduce(lambda a, b: (0, a[1] + b[1]), items)[1]
您當然可以弄清楚,但需要時間來解開表示式以瞭解發生了什麼。使用簡短的巢狀 def
語句會好一點
import functools
def combine(a, b):
return 0, a[1] + b[1]
total = functools.reduce(combine, items)[1]
但如果我直接使用 for
迴圈,那將是最好的
total = 0
for a, b in items:
total += b
或者內建的 sum()
和一個生成器表示式
total = sum(b for a, b in items)
functools.reduce()
的許多用法,當寫成 for
迴圈時,會更清晰。
Fredrik Lundh 曾提出以下一組規則來重構 lambda
的用法
編寫一個 lambda 函式。
寫一段註釋,解釋這個 lambda 到底做了什麼。
仔細研究註釋,並想一個能捕捉註釋精髓的名字。
將 lambda 轉換為 def 語句,使用該名字。
刪除註釋。
我非常喜歡這些規則,但您完全可以不同意這種無 lambda 風格是否更好。
修訂歷史和致謝¶
作者感謝以下人員為本文的各種草稿提供了建議、更正和協助: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 日釋出。
0.11 版本:2006 年 7 月 1 日釋出。修正了錯別字。
0.2 版本:2006 年 7 月 10 日釋出。將 genexp 和 listcomp 部分合併為一章。修正了錯別字。
0.21 版本:增加了導師郵件列表中建議的更多參考文獻。
0.30 版本:增加了由 Collin Winter 編寫的 functional
模組一節;增加了 operator 模組的簡短介紹;其他一些編輯。
參考文獻¶
通用¶
**計算機程式的構造和解釋**,Harold Abelson 和 Gerald Jay Sussman 著,Julie Sussman 協助。該書可在 https://mitpress.mit.edu/sicp 找到。在這本經典的計算機科學教科書中,第 2 章和第 3 章討論了使用序列和流來組織程式內的資料流。該書使用 Scheme 作為示例,但這些章節中描述的許多設計方法適用於函式式風格的 Python 程式碼。
https://defmacro.org/2006/06/19/fp.html:一篇通用的函數語言程式設計介紹,使用 Java 示例,並有詳細的歷史介紹。
https://en.wikipedia.org/wiki/Functional_programming:描述函數語言程式設計的通用維基百科條目。
https://en.wikipedia.org/wiki/Coroutine:協程的條目。
https://en.wikipedia.org/wiki/Partial_application:部分函式應用概念的條目。
https://en.wikipedia.org/wiki/Currying:柯里化概念的條目。
Python 特有¶
https://gnosis.cx/TPiP/:David Mertz 的書《Python 中的文字處理》第一章在“在文字處理中利用高階函式”一節中討論了用於文字處理的函數語言程式設計。
Mertz 還為 IBM 的 DeveloperWorks 網站撰寫了一個關於函數語言程式設計的 3 部分系列文章;請參閱第 1 部分、第 2 部分和第 3 部分,
Python 文件¶
itertools
模組的文件。
functools
模組的文件。
operator
模組的文件。
**PEP 289**:“生成器表示式”
**PEP 342**:“透過增強生成器實現協程”描述了 Python 2.5 中的新生成器特性。