函數語言程式設計 HOWTO

作者:

A. M. Kuchling

釋出:

0.32

在本文件中,我們將瀏覽 Python 的特性,這些特性適用於以函式式風格實現程式。在介紹了函數語言程式設計的概念之後,我們將研究諸如 迭代器生成器 之類的語言特性,以及諸如 itertoolsfunctools 之類的相關庫模組。

簡介

本節解釋了函數語言程式設計的基本概念;如果您只是對了解 Python 語言特性感興趣,請跳到下一節關於 迭代器 的內容。

程式語言支援以幾種不同的方式分解問題

  • 大多數程式語言都是 過程式 的:程式是指令列表,告訴計算機如何處理程式的輸入。C、Pascal 甚至 Unix shell 都是過程式語言。

  • 宣告式 語言中,您編寫一個規範來描述要解決的問題,而語言實現則會弄清楚如何有效地執行計算。SQL 是您最有可能熟悉的宣告式語言;SQL 查詢描述了您想要檢索的資料集,SQL 引擎決定是掃描表還是使用索引,應該首先執行哪些子句等。

  • 面向物件 的程式操作物件集合。物件具有內部狀態,並支援以某種方式查詢或修改此內部狀態的方法。Smalltalk 和 Java 是面向物件的語言。C++ 和 Python 是支援面向物件程式設計的語言,但不強制使用面向物件的功能。

  • 函式式 程式設計將問題分解為一組函式。理想情況下,函式只接受輸入併產生輸出,並且沒有任何影響給定輸入產生的輸出的內部狀態。著名的函式式語言包括 ML 系列(Standard ML、OCaml 和其他變體)和 Haskell。

某些計算機語言的設計者選擇強調一種特定的程式設計方法。這通常使得編寫使用不同方法的程式變得困難。其他語言是多正規化語言,支援幾種不同的方法。Lisp、C++ 和 Python 是多正規化語言;您可以使用所有這些語言編寫在很大程度上是過程式的、面向物件的或函式式的程式或庫。在一個大型程式中,不同的部分可能會使用不同的方法編寫;例如,GUI 可能是面向物件的,而處理邏輯是過程式的或函式式的。

在函式式程式中,輸入流經一組函式。每個函式對其輸入進行操作併產生一些輸出。函式式風格不鼓勵具有修改內部狀態或進行其他在函式的返回值中不可見的更改的副作用的函式。根本沒有副作用的函式稱為 純函式式。避免副作用意味著不使用隨著程式執行而更新的資料結構;每個函式的輸出必須僅取決於其輸入。

有些語言對純度要求非常嚴格,甚至沒有諸如 a=3c = a + b 之類的賦值語句,但很難避免所有副作用,例如列印到螢幕或寫入磁碟檔案。另一個示例是呼叫 print()time.sleep() 函式,它們都不返回有用的值。呼叫它們只是為了它們的副作用,即將一些文字傳送到螢幕或暫停執行一秒鐘。

以函式式風格編寫的 Python 程式通常不會走極端,避免所有 I/O 或所有賦值;相反,它們將提供一個外觀函式式的介面,但在內部使用非函式式的功能。例如,函式的實現仍然會使用對區域性變數的賦值,但不會修改全域性變數或具有其他副作用。

函數語言程式設計可以被認為是面向物件程式設計的對立面。物件是包含一些內部狀態以及一組允許您修改此狀態的方法呼叫的小膠囊,程式由進行正確的狀態更改組成。函數語言程式設計希望儘可能避免狀態更改,並使用在函式之間流動的資料。在 Python 中,您可以透過編寫接受並返回表示應用程式中物件(電子郵件訊息、事務等)的例項的函式來組合這兩種方法。

函式式設計可能看起來像是一個奇怪的約束。為什麼要避免物件和副作用?函式式風格具有理論和實踐上的優勢

  • 形式可證明性。

  • 模組化。

  • 可組合性。

  • 易於除錯和測試。

形式可證明性

一個理論上的好處是,更容易構造一個數學證明來證明函式式程式是正確的。

長期以來,研究人員一直對尋找數學方法來證明程式正確性感興趣。這與在大量輸入上測試程式並得出結論認為其輸出通常是正確的,或讀取程式的原始碼並得出結論認為程式碼看起來是正確的不同;相反,目標是嚴格證明程式對所有可能的輸入都產生正確的結果。

用於證明程式正確的技術是寫下 不變式,即輸入資料和程式變數的始終為真的屬性。對於每一行程式碼,您然後顯示如果 執行該行程式碼 之前 不變式 X 和 Y 為真,則 執行該行程式碼 之後 略有不同的不變式 X' 和 Y' 為真。這種情況會一直持續到程式結束,此時不變式應與程式輸出的期望條件相匹配。

函數語言程式設計對賦值的避免產生的原因是,使用這種技術很難處理賦值;賦值可能會破壞賦值之前為真的不變式,而不會產生任何可以向前傳播的新不變式。

不幸的是,證明程式正確在很大程度上是不切實際的,並且與 Python 軟體無關。即使是簡單的程式也需要數頁長的證明;一箇中等複雜程式的正確性證明將是巨大的,並且您每天使用的程式(Python 直譯器、XML 解析器、Web 瀏覽器)很少或沒有可以證明是正確的。即使您寫下或生成了證明,也會有驗證證明的問題;也許其中存在錯誤,並且您錯誤地認為您已經證明了該程式是正確的。

模組化

函數語言程式設計更實際的好處是它迫使您將問題分解為小塊。程式因此更模組化。指定和編寫一個做一件事的小函式比編寫一個執行復雜轉換的大函式更容易。小函式也更容易閱讀和檢查錯誤。

易於除錯和測試

測試和除錯函式式風格的程式更容易。

除錯得以簡化,因為函式通常很小且明確指定。當程式無法工作時,每個函式都是一個介面點,您可以在其中檢查資料是否正確。您可以檢視中間輸入和輸出,以快速隔離導致 bug 的函式。

測試更容易,因為每個函式都是單元測試的潛在主題。函式不依賴於需要在執行測試之前複製的系統狀態;相反,您只需要合成正確的輸入,然後檢查輸出是否符合預期。

可組合性

當您處理函式式風格的程式時,您將編寫許多具有不同輸入和輸出的函式。其中一些函式將不可避免地專門用於特定的應用程式,但其他函式將在各種程式中都很有用。例如,一個接受目錄路徑並返回該目錄中所有 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 子句都是可選的;如果存在,則僅當 condition 為真時,才會計算 expression 並將其新增到結果中。

生成器表示式必須始終寫在圓括號內,但表示函式呼叫的圓括號也算數。如果您想建立一個將立即傳遞給函式的迭代器,您可以編寫

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

for...in 子句包含要迭代的序列。這些序列不必長度相同,因為它們是從左到右迭代的,**不是**並行迭代的。對於 sequence1 中的每個元素,將從頭開始迴圈訪問 sequence2。然後,對於 sequence1sequence2 中的每個結果元素對,迴圈訪問 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 語句。 yieldreturn 語句之間的巨大區別在於,當到達 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 會導致 StopIteration(value)__next__() 方法中引發。一旦發生這種情況,或者到達函式的底部,值的程序就會結束,並且生成器無法產生任何進一步的值。

您可以透過編寫自己的類並將生成器的所有區域性變數儲存為例項變數來手動實現生成器的效果。例如,返回整數列表可以透過將 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 皇后問題(將 N 個皇后放置在 NxN 的棋盤上,使任何一個皇后都不能威脅到另一個皇后)和騎士巡遊(找到一條騎士到達 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 異常以終止迭代。在收到此異常時,生成器的程式碼必須引發 GeneratorExitStopIteration;捕獲異常並執行其他任何操作都是非法的,並且會觸發 RuntimeErrorclose() 也會在生成器被垃圾回收時由 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) 返回一個迭代器,該迭代器包含所有滿足特定條件的序列元素,並且類似地可以透過列表推導式複製。predicate 是一個返回某個條件真值的函式;要與 filter() 一起使用,predicate 必須接受單個值。

>>> 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) 將可迭代物件的所有元素收集到一個列表中,對列表進行排序,並返回排序後的結果。 keyreverse 引數會傳遞給構造列表的 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 的字串和列表切片不同,您不能對 startstopstep 使用負值。

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) 接受兩個迭代器,並且僅返回 dataselectors 的對應元素為 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() 假設底層可迭代物件的內容將已根據鍵排序。請注意,返回的迭代器也使用底層可迭代物件,因此你必須先使用 iterator-1 的結果,然後再請求 iterator-2 及其對應的鍵。

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 用法的規則:

  1. 編寫一個 lambda 函式。

  2. 寫一段註釋,解釋這個 lambda 函式到底在做什麼。

  3. 研究註釋一會兒,並想出一個能夠捕捉註釋本質的名稱。

  4. 使用該名稱,將 lambda 函式轉換為 def 語句。

  5. 刪除註釋。

我真的很喜歡這些規則,但你可以自由地決定是否這種無 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:添加了 tutor 郵件列表中建議的更多參考資料。

版本 0.30:添加了 Collin Winter 編寫的關於 functional 模組的部分;添加了關於 operator 模組的簡短部分;以及其他一些編輯。

參考資料

通用

計算機程式的構造和解釋,作者是 Harold Abelson 和 Gerald Jay Sussman,以及 Julie Sussman。本書可以在 https://mitpress.mit.edu/sicp 找到。在這本經典的計算機科學教科書中,第 2 章和第 3 章討論瞭如何使用序列和流來組織程式內部的資料流。本書使用 Scheme 作為示例,但這些章節中描述的許多設計方法都適用於函式式 Python 程式碼。

https://www.defmacro.org/ramblings/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 中的新生成器功能。