作為參數
如果你對OOP的模板方法模式很熟悉,相信你能很快速地學會將函數當作參數傳遞。兩者大體是一致的,只是在這裡,我們傳遞的是函數本身而不再是實現了某個介面的對象。
我們先來給前面定義的求和函數add熱熱身:
?
1 |
print add( '三角形的樹' , '北極' ) |
與加法運算子不同,你一定很驚訝於答案是'三角函數'。這是一個內建的彩蛋...bazinga!
言歸正傳。我們的客戶有一個從0到4的列表:
?
1 |
lst = range ( 5 ) #[0, 1, 2, 3, 4] |
雖然我們在上一小節裡給了他一個加法器,但現在他仍然在為如何計算這個列表所有元素的和而苦惱。當然,對我們而言這個任務輕鬆極了:
?
123 |
amount = 0 for num in lst: amount = add(amount, num) |
這是一段典型的指令式風格的代碼,一點問題都沒有,肯定可以得到正確的結果。現在,讓我們試著用函數式的風格重構一下。
首先可以預見的是求和這個動作是非常常見的,如果我們把這個動作抽象成一個單獨的函數,以後需要對另一個列表求和時,就不必再寫一遍這個套路了:
?
1234567 |
def sum_(lst): amount = 0 for num in lst: amount = add(amount, num) return amount print sum_(lst) |
還能繼續。sum_函數定義了這樣一種流程:
1. 使用初始值與列表的第一個元素相加;
2. 使用上一次相加的結果與列表的下一個元素相加;
3. 重複第二步,直到列表中沒有更多元素;
4. 將最後一次相加的結果返回。
如果現在需要求乘積,我們可以寫出類似的流程——只需要把相加換成相乘就可以了:
?
12345 |
def multiply(lst): product = 1 for num in lst: product = product * num return product |
除了初始值換成了1以及函數add換成了乘法運算子,其他的代碼全部都是冗餘的。我們為什麼不把這個流程抽象出來,而將加法、乘法或者其他的函數作為參數傳入呢?
?
1234567 |
def reduce_(function, lst, initial): result = initial for num in lst: result = function(result, num) return result print reduce_(add, lst, 0 ) |
現在,想要算出乘積,可以這樣做:
?
1 |
print reduce_( lambda x, y: x * y, lst, 1 ) |
那麼,如果想要利用reduce_找出列表中的最大值,應該怎麼做呢?請自行思考:)
雖然有模板方法這樣的設計模式,但那樣的複雜度往往使人們更情願到處編寫迴圈。將函數作為參數完全避開了模板方法的複雜度。
Python有一個內建函數reduce,完整實現並擴充了reduce_的功能。本文稍後的部分包含了有用的內建函數的介紹。請注意我們的目的是沒有迴圈,使用函數替代迴圈是函數式風格區別於指令式風格的最顯而易見的特徵。
*像Python這樣構建於類C語言之上的函數式語言,由於語言本身提供了編寫迴圈代碼的能力,內建函數雖然提供函數式編程的介面,但一般在內部還是使用迴圈實現的。同樣的,如果發現內建函數無法滿足你的迴圈需求,不妨也封裝它,並提供一個介面。
作為傳回值
將函數返回通常需要與閉包一起使用(即返回一個閉包)才能發揮威力。我們先看一個函數的定義:
?
12345 |
def map_(function, lst): result = [] for item in lst: result.append(function(item)) return result |
函數map_封裝了最常見的一種迭代:對列表中的每個元素調用一個函數。map_需要一個函數參數,並將每次調用的結果儲存在一個列表中返回。這是指令式的做法,當你知道了列表解析(list comprehension)後,會有更好的實現。
這裡我們先略過map_的蹩腳實現而只關注它的功能。對於上一節中的lst,你可能發現最後求乘積結果始終是0,因為lst中包含了0。為了讓結果看起來足夠大,我們來使用map_為lst中的每個元素加1:
?
12 |
lst = map_( lambda x: add( 1 , x), lst) print reduce_( lambda x, y: x * y, lst, 1 ) |
答案是120,這還遠遠不夠大。再來:
?
12 |
lst = map_( lambda x: add( 10 , x), lst) print reduce_( lambda x, y: x * y, lst, 1 ) |
囧,事實上我真的沒有想到答案會是360360,我發誓沒有收周鴻禕任何好處。
現在回頭看看我們寫的兩個lambda運算式:相似性超過90%,絕對可以使用抄襲來形容。而問題不在於抄襲,在於多寫了很多字元有木有?如果有一個函數,根據你指定的左運算元,能產生一個加法函數,用起來就像這樣:
?
1 |
lst = map_(add_to( 10 ), lst) #add_to(10)返回一個函數,這個函數接受一個參數並加上10後返回 |
寫起來應該會舒服不少。下面是函數add_to的實現:
?
12 |
def add_to(n): return lambda x: add(n, x) |
通過為已經存在的某個函數指定數個參數,產生一個新的函數,這個函數只需要傳入剩餘未指定的參數就能實現原函數的全部功能,這被稱為偏函數。Python內建的functools模組提供了一個函數partial,可以為任意函數產生偏函數:
?
1 |
functools.partial(func[, * args][, * * keywords]) |
你需要指定要產生偏函數的函數、並且指定數個參數或者具名引數,然後partial將返回這個偏函數;不過嚴格的說partial返回的不是函數,而是一個像函數一樣可直接調用的對象,當然,這不會影響它的功能。
另外一個特殊的例子是裝飾器。裝飾器用於增強甚至乾脆改變原函數的功能,我曾寫過一篇文檔介紹裝飾器,地址在這裡:http://www.cnblogs.com/huxi/archive/2011/03/01/1967600.html。
*題外話,單就例子中的這個功能而言,在一些其他的函數式語言中(例如Scala)可以使用名為柯裡化(Currying)的技術實現得更優雅。柯裡化是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,並且返回接受餘下的參數而且返回結果的新函數的技術。如下的虛擬碼所示:
?
12345 |
#不是真實的代碼 def add(x)(y): #柯裡化 return x + y lst = map_(add( 10 ), lst) |
通過將add函數柯裡化,使得add接受第一個參數x,並返回一個接受第二個參數y的函數,調用該函數與前文中的add_to完全相同(返回x + y),且不再需要定義add_to。看上去是不是更加清爽呢?遺憾的是Python並不支援柯裡化。
部分內建函數介紹
- reduce(function, iterable[, initializer])
這個函數的主要功能與我們定義的reduce_相同。需要補充兩點:
它的第二個參數可以是任何可迭代的對象(實現了__iter__()方法的對象);
如果不指定第三個參數,則第一次調用function將使用iterable的前兩個元素作為參數。
由reduce和一些常見的function組合成了下面列出來的內建函數:?
12345 |
all (iterable) = = reduce ( lambda x, y: bool (x and y), iterable) any (iterable) = = reduce ( lambda x, y: bool (x or y), iterable) max (iterable[, args...][, key]) = = reduce ( lambda x, y: x if key(x) > key(y) else y, iterable_and_args) min (iterable[, args...][, key]) = = reduce ( lambda x, y: x if key(x) < key(y) else y, iterable_and_args) sum (iterable[, start]) = = reduce ( lambda x, y: x + y, iterable, start) |
- map(function, iterable, ...)
這個函數的主要功能與我們定義的map_相同。需要補充一點:
map還可以接受多個iterable作為參數,在第n次調用function時,將使用iterable1[n], iterable2[n], ...作為參數。
- filter(function, iterable)
這個函數的功能是過濾出iterable中所有以元素自身作為參數調用function時返回True或bool(傳回值)為True的元素並以列表返回,與系列第一篇中的my_filter函數相同。
- zip(iterable1, iterable2, ...)
這個函數返回一個列表,每個元素都是一個元組,包含(iterable1[n], iterable2[n], ...)。
例如:zip([1, 2], [3, 4]) --> [(1, 3), (2, 4)]
如果參數的長度不一致,將在最短的序列結束時結束;如果不提供參數,將返回空列表。
除此之外,你還可以使用本文2.5節中提到的functools.partial()為這些內建函數建立常用的偏函數。
另外,pypi上有一個名為functional的模組,除了這些內建函數外,還額外提供了更多的有意思的函數。但由於使用的場合并不多,並且需要額外安裝,在本文中就不介紹了。但我仍然推薦大家下載這個模組的純Python實現的原始碼看看,開闊思維嘛。裡面的函數都非常短,源檔案總共只有300行不到,地址在這裡:http://pypi.python.org/pypi/functional