Python函數(shù)式編程

本文翻譯自Functional Programming Howto

lambda

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

函數(shù)式編程

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

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

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

設(shè)計計算機語言時,設(shè)計者會選擇強調(diào)一種特定的編程方法。這通常會導(dǎo)致采用另外的方法編寫程序會變得困難。有一些其他的語言是支持多種不同方法的多范式語言。Lisp,C++和Python是多范式語言;使用這些語言可以編寫面向過程,面向?qū)ο?,或者函?shù)式的程序或者庫。在一個大型程序中,可能會使用不同的方法來編寫不同的部分。比如,程序中的GUI部分采用面向?qū)ο蠓椒?,而處理的邏輯是面向過程或者是函數(shù)式的。

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

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

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

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

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

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

正式證明性

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

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

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

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

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

模塊化

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

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

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

函數(shù)通暢很小并且功能明確,所以調(diào)試起來會很方便。當(dāng)程序無法運行時,可以在每個函數(shù)入口檢查數(shù)據(jù)是否正確??梢酝ㄟ^查看中間輸入和輸出,來快速定位出現(xiàn)bug的函數(shù)。

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

組合性

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

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

Python特性

迭代器

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

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

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

可以體驗一下迭代接口:

>>> L = [1, 2, 3]
>>> it = iter(L)
>>> print it
<listiterator object at 0x100bc3950>
>>> it.next()
1
>>> it.next()
2
>>> it.next()
3
>>> it.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

在一些不同的上下文中,Python期望對象是可迭代的,其中最重要的就是for語句。在語句for x in y中,Y必須是迭代器或者能通過iter()方法生成一個迭代器的對象。以下兩條語句是等價的:

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

可以通過list()tuple()構(gòu)造方法操作迭代器得到列表或者是元組。

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

序列解包也支持迭代器:如果知道迭代器將返回N個元素,則可以將其解包為N元組:

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

內(nèi)置的max()min()方法接受迭代器作為參數(shù),返回最大或者最小的元素。"in""not in"操作同樣支持迭代器:如果X在迭代器返回的流中,則X in iterator返回true。如果迭代器是無限的話,明顯會有一些問題;max()min()將永遠不會返回,如果X沒有出現(xiàn)在流中,innot in也將不會返回。

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

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

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

對一個字典調(diào)用iter()方法將會返回一個迭代器,該迭代器循環(huán)使用字典的鍵。

>>> m = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6, 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
>>> for key in m:
...     print key, m[key]
...
Feb 2
Aug 8
Jan 1
Dec 12
Oct 10
Mar 3
Sep 9
May 5
Jun 6
Jul 7
Apr 4
Nov 11

請注意,上面輸出的順序是隨機的,因為排序是基于字典對象的哈希順序。

對字典對象使用iter()方法會返回鍵的迭代器,但是字典有其他方法得到不同的迭代器。如果想迭代鍵,值,鍵/值對,分別可以調(diào)用iterkeys(),itervalues(),iteritems()來的得到對應(yīng)的迭代器。

dict()構(gòu)造方法可以接受返回(key, value)元組流的迭代器,生成新的字典。

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

文件也可以通過readline()方法來迭代文件的內(nèi)容,也就是說可以通過下面的方式來讀取文件的每一行:

for line in file:
    ...

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

set(['Italy', 'US', 'France'])
>>> S = set((2, 3, 5, 7, 11, 13))
>>> for i in S:
...     print i
...
2
3
5
7
11
13

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

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

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

列表推導(dǎo)和生成器表達式是這種操作的簡寫符號,這是從Haskell中借鑒來的??梢酝ㄟ^下面的代碼從字符串流中去掉所有空格:

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

# 生成器表達式,返回迭代器
stripped_iter = (line.strip() for line in line_list)

# 列表推導(dǎo)式,返回列表
stripped_list = [line.strip() for line in line_list]

可以通過添加"if"條件來篩選特定的元素。

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

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

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

( expression for expr in sequence1
             if condition1
             for expr2 in sequence2
             if condition2
             for expr3 in sequence3 ...
             if condition3
             for exprN in sequenceN
             if conditionN )

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

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

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

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

子語句for...in包含要迭代的序列。哪些序列的長度不必相同,因為迭代的順序是從左到右,不是并行的。對sequence1中的每個元素,sequence2都會從頭迭代。然后對sequence1sequence2的每個結(jié)果對循環(huán)迭代sequence3。

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

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

            # 輸出表達式的值

這意味著,當(dāng)有多個for...in子語句但沒有if條件的情況下,所得到的輸出的長度將等于所有序列長度的乘積。如果有兩個長度為3的列表,結(jié)果的長度就是9。

>>> seq1 = 'abc'
>>> seq2 = (1, 2, 3)
>>> [(x,y) for x in seq1 for y in seq2]
[('a', 1), ('a', 2), ('a', 3), ('b', 1), ('b', 2), ('b', 3), ('c', 1), ('c', 2), ('c', 3)]

為了不引起Python語法上的歧義,如果expression用來生成一個元組,則必須將其用()括起來。下面例子中第一個有語法錯誤,第二個是正確的。

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

生成器

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

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

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

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

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

當(dāng)生成器函數(shù)被調(diào)用時,它不會返回單個值,而是會返回支持迭代器協(xié)議的生成器對象。在執(zhí)行yield語句時,生成器輸出i的值,這和return語句類似。yieldreturn語句之間的最大區(qū)別在于,在到達yield時,生成器的執(zhí)行狀態(tài)將被暫停,并保留局部變量。在下一次調(diào)用生成器的.next()方法時,該函數(shù)將繼續(xù)執(zhí)行。

下面是generate_ints()生成器的使用方法:

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

寫成for i in generate_ints(3)或者a,b,c=generate_ints(3)也是一樣的。

在生成器函數(shù)中,return語句只能在沒有值的情況下使用,并且表示值的結(jié)束。在執(zhí)行return語句后,生成器不能再返回更多的值。如果在生成器函數(shù)中return帶了返回值,比如return 5,會被認為是語法錯誤??梢酝ㄟ^手動拋出StopIteration異常,或者讓函數(shù)執(zhí)行到最后,來讓生成器函數(shù)不再產(chǎn)生新的值。
可以通過自定義的類,將生成器的所有本地變量存儲為實例,來達到生成器的效果。比如,通過將self.count設(shè)置為0,將self.next()實現(xiàn)為自增self.count并返回,來返回一列整數(shù)。然而,對于一個有一定復(fù)雜度的生成器,實現(xiàn)自定義的類會更麻煩。

Python測試套件中的test_generator.py有更多有意思的例子。下面是一個遞歸實現(xiàn)樹的順序遍歷的生成器。

def inorder(t):
    if t:
        for x in inorder(t.left):
            yield x
        
        yield t.label
        
        for x in inorder(t.right):
            yield x

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

向生成器傳值

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

在Python2.5中,有一個簡單的方法將值傳遞給生成器。yield成為了表達式,返回一個可以分配給變量或者以其他方式運行的值。

val = (yield i)

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

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

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

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

下面是改變計數(shù)器的方法:

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

因為yield會經(jīng)常返回None,代碼中應(yīng)該檢查這種情況。不要在表達式中使用這個值,除非確定send()是恢復(fù)函數(shù)的唯一方法。

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

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

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

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

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

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

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

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

map(f, iterA, iterB, ...)返回一個列表,內(nèi)容是f(iterA[0], iterB[0]), f(iterA[1], iterB[1]), f(iterA[2], iterB[2]), ...

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

正如上面所展示的,使用列表推導(dǎo)式可以實現(xiàn)一樣的效果。itertools.imap()完成一樣的功能,不過它可以處理無盡的迭代器;等會在討論itertools模塊的再談。

filter(predicate, iter)返回一個包含滿足特定條件的所有序列元素的列表,這個功能和列表推導(dǎo)重復(fù)。predicate是返回一些條件的真值的函數(shù);為了和filter()一起使用,predicate只能傳入一個參數(shù)。

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

上面的功能也可以使用列表推導(dǎo)式完成:

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

filter()itertools模塊中也有對應(yīng)的方法,itertools.ifilter,這個方法返回一個迭代器,因此也可以像itertools.imap()一樣處理無限序列。

reduce(func, iter, [initial_value])itertools模塊中沒有對應(yīng)的方法,因為它累積地對可迭代對象的所有元素執(zhí)行操作,因此不能用于無限迭代。func函數(shù)必須接受兩個參數(shù)并返回一個值。reduce()接受迭代器返回的前兩個元素A和B,返回func(A, B)。之后請求第三個元素C,計算func(func(A, B), C),然后請求迭代器返回的第四個元素,持續(xù)這樣的步驟一直到迭代完所有元素。如果可迭代對象不返回任何元素,會拋出TypeError異常。如果提供了初始值,第一輪運算會是func(initial_value, A)。

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

如果在reduce()中使用operator.add(),會得到迭代對象所有值的和。這個場景使用非常廣泛,有內(nèi)置的sum()方法來計算。

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

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

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

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

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

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

循環(huán)遍歷列表并記錄滿足某些條件的索引時,常常使用enumerate()

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

sorted(iterable, [cmp=None], [key=None], [reverse=False])將可迭代對象中的所有元素收集到列表中,對列表進行排序,并返回排序結(jié)果。cmp,keyreverse參數(shù)傳遞給構(gòu)造的列表的.sort()方法。

>>> import random
>>> rand_list = random.sample(range(10000), 8)
>>> rand_list
[3027, 8533, 16, 6602, 4183, 9577, 4842, 5713]
>>> sorted(rand_list)
[16, 3027, 4183, 4842, 5713, 6602, 8533, 9577]
>>> sorted(rand_list, reverse=True)
[9577, 8533, 6602, 5713, 4842, 4183, 3027, 16]

內(nèi)置的any(iter)all(iter)函數(shù)查看一個可迭代對象的真值。如果有任意元素為真值,any()返回True,如果所有元素都為真值,all()返回True。

>>> any([0,1,0])
True
>>> any([0,0,0])
False
>>> any([1,1,1])
True
>>> all([0,1,0])
False
>>> all([0,0,0])
False
>>> all([1,1,1])
True

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

編寫函數(shù)式程序時,通常需要很少的功能作為謂詞或以某種方式組合元素。

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

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

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

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

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

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

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

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

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

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

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

total = reduce(combine, items)[1]

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

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

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

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

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

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

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

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

itertools模塊

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

該模塊的功能分為幾類:

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

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

函數(shù)itertools.count(n)返回?zé)o限的整數(shù)流,每次增加1。可以選擇起始的數(shù)字,默認是0:

itertools.count() =>
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
itertools.count(10) =>
    10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...

函數(shù)itertools.cycle(iter)保存可迭代對象內(nèi)容的副本,返回一個新的迭代器,該迭代器從頭到尾返回其元素。新的迭代器將無限重復(fù)這些元素。

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

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

itertools.repeat('abc') =>
    abc, abc, abc, abc, abc, abc, abc, abc, abc, ...
itertools.repeat('abc', 5) =>
    abc, abc, abc, abc, abc

itertools.chain(iterA, iterB, ...)輸入任意數(shù)量的可迭代對象,返回第一個迭代器的所有元素,然后返回第二個迭代器的所有元素,依次類推,知道返回最有一個迭代器的所有元素。

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

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

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

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

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

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

但是應(yīng)該避免這樣做,因為從更長的可迭代對象中讀取的元素可能會被丟棄。這意味著不能進一步使用該可迭代對象,因為有跳過丟棄元素的風(fēng)險。

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

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

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

對可迭代對象的元素進行操作

有兩個函數(shù)用于對可迭代的內(nèi)容進行其他函數(shù)的調(diào)用。

itertools.imap(f, iterA, iterB)返回包含f(iterA[0], iterB[0]), f(iterA[1], iterB[1]), f(iterA[2], iterB[2]), ...的流:

itertools.imap(operator.add, [5, 6, 5], [1, 2, 3]) =>
    6, 8, 8

operator模塊包含一組與Python運算符相對應(yīng)的函數(shù)。比如operator.add(a, b)(兩個元素相加),operator.ne(a, b)(等同于a!=b),和operator.attrgetter('id')(返回一個可以獲取"id"屬性的可調(diào)用方法)。

itertools.starmap(func, iter)假定可迭代對象返回一個元組流,并使用這些元組作為參數(shù)調(diào)用f():

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

選擇元素

另一組函數(shù)根據(jù)謂詞選擇迭代器元素的子集。

itertools.ifilter(predicate, iter)返回所有predicate為真的元素:

def is_even(x):
    return (x % 2) == 0
    
itertools.ifilter(is_even, itertools.count()) =>
    0, 2, 4, 6, 8, 10, 12, 14, ..

itertools.ifilterfalse(predicate, iter)正好相反,返回所有為假的元素:

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

itertools.takewhile(predicate, iter)只要predicate為真,就一直返回值。一旦predicate為假,就停止迭代。

def less_than_10(x):
    return (x < 10)
    
itertools.takewhile(less_than_10, itertoosl.count()) =>
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9

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

itertools.dropwhile(predicate, iter)丟棄所有predicate為真的值,返回其他值。

itertools.dropwhile(less_than_10, itertools.count()) =>
    10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...
    
itertools.dropwhile(is_even, itertools.count()) =>
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...

元素分組

現(xiàn)在談的最后一個函數(shù)itertools.groupby(iter, key_func=None)是最復(fù)雜的。key_func(elem)是一個可以計算可迭代對象返回的每個元素鍵值的函數(shù)。如果沒有傳入這個函數(shù),鍵值就是元素本身。

函數(shù)groupby()從具有相同鍵值的底層迭代中收集所有連續(xù)元素,并返回一個包含鍵值的2元組流河具有該鍵的元素的迭代器。

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

def get_state((city, state)):
    return state
    
itertools.groupby(city_list, get_state) =>
    ('AL', iterator-1),
    ('AK', iterator-2),
    ('AZ', iterator-3), ...
    
iterator-1 =>
    ('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL')
iterator-2 =>
    ('Anchorage', 'AK'), ('Nome', 'AK')
iterator-3 =>
    ('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ')

函數(shù)groupby()假定底層可迭代對象的內(nèi)容已經(jīng)根據(jù)鍵值進行排序。請注意,返回的迭代器依然使用底層的可迭代對象,因此必須在消費iterator-1中的內(nèi)容后,再去請求iterator-2及其相應(yīng)的鍵值。

functools模塊

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

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

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

下面是一個小例子:

import functools

def log(message, subsystem):
    # 向特定的system寫入消息
    print '%s: %s' % (subsystem, message)
    ...
    
server_log = functools.partial(log, subsystem='server')
server_log('Unable to open socket')

operator模塊

之前提到過operator模塊。它包含一組于Python操作符相對應(yīng)的函數(shù)。這些函數(shù)在函數(shù)式風(fēng)格的代碼中通常很有用,因為它可以替代一些只包括單個操作的函數(shù)。

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

  • 數(shù)學(xué)運算:add(),sub(),mul(),div(),floordiv(),abs(),...
  • 邏輯運算:not_(),truth()。
  • 位運算:and_(),or_(),invert()
  • 對比:eq(),ne(),lt(),le(),gt()ge()。
  • 對象標識:is_(),is_not()。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容