可愛的Python函數(shù)式編程(三)
摘要:作者David Mertz在其文章《可愛的Python:“Python中的函數(shù)式編程”》中的第一部分和第二部分中觸及了函數(shù)式編程的大量基本概念。本文中他將繼續(xù)前面的討論,解釋函數(shù)式編程的其它功能,如currying和Xoltar Toolkit中的其它一些高階函數(shù)。
表達式綁定
有一位從不滿足于解決部分問題讀者,名叫Richard Davies,提出了一個問題,問是否可以將所有的綁定全部都轉(zhuǎn)移到一個單個的表達式之中。首先讓我們簡單看看,我們?yōu)槭裁聪脒@么做,然后再看看由comp.lang.python中的一位朋友提供的一種異常優(yōu)雅地寫表達式的方式。
讓我們回想一下功能模塊的綁定類。使用該類的特性,我們可以確認(rèn)在一個給定的范圍塊內(nèi),一個特定的名字僅僅代表了一個唯一的事物。
具有重新綁定向?qū)У?Python 函數(shù)式編程(FP)
- >>> from functional import *
- >>> let = Bindings()
- >>> let.car = lambda lst: lst[0]
- >>> let.car = lambda lst: lst[2]
- Traceback (innermost last):
- File "<stdin>", line 1, in ?
- File "d:\tools\functional.py", line 976, in __setattr__
- raise BindingError, "Binding '%s' cannot be modified." % name
- functional.BindingError: Binding 'car' cannot be modified.
- >>> let.car(range(10))
- 0
綁定類在一個模塊或者一個功能定義范圍內(nèi)做這些我們希望的事情,但是沒有辦法在一條表達式內(nèi)使之工作。然而在ML家族語言(譯者注:ML是一種通用的函數(shù)式編程語言),在一條表達式內(nèi)創(chuàng)建綁定是很自然的事。
Haskell 命名綁定表達式
- -- car (x:xs) = x -- *could* create module-level binding
- list_of_list = [[1,2,3],[4,5,6],[7,8,9]]
- -- 'where' clause for expression-level binding
- firsts1 = [car x | x <- list_of_list] where car (x:xs) = x
- -- 'let' clause for expression-level binding
- firsts2 = let car (x:xs) = x in [car x | x <- list_of_list]
- -- more idiomatic higher-order 'map' technique
- firsts3 = map car list_of_list where car (x:xs) = x
- -- Result: firsts1 == firsts2 == firsts3 == [1,4,7]
Greg Ewing 發(fā)現(xiàn)用Python的list概念實現(xiàn)同樣的效果是有可能的;甚至我們可以用幾乎與Haskell語法一樣干凈的方式做到。
Python 2.0+ 命名綁定表達式
- >>> list_of_list = [[1,2,3],[4,5,6],[7,8,9]]
- >>> [car_x for x in list_of_list for car_x in
- (x[0],)]
- [1, 4, 7]
在列表解析(list comprehension)中將表達式放入一個單項元素(a single-item tuple)中的這個小技巧,并不能為使用帶有表達式級綁定的高階函數(shù)提供任何思路。要使用這樣的高階函數(shù),還是需要使用塊級(block-level)綁定,就象以下所示:
Python中的使用塊級綁定的'map()'
- >>> list_of_list = [[1,2,3],[4,5,6],[7,8,9]]
- >>> let = Bindings()
- >>> let.car = lambda l: l[0]
- >>> map(let.car,list_of_list)
- [1, 4, 7]
這樣真不錯,但如果我們想使用函數(shù)map(),那么其中的綁定范圍可能會比我們想要的更寬一些。然而,我們可以做到的,哄騙列表解析讓它替我們做名字綁定,即使其中的列表并不是我們最終想要得到的列表的情況下也沒問題:
從Python的列表解析中“走下舞臺”
- # Compare Haskell expression:
- # result = func car_car
- # where
- # car (x:xs) = x
- # car_car = car (car list_of_list)
- # func x = x + x^2
- >>> [func for x in list_of_list
- ...
- for car in (x[0],)
- ...
- for func in (car+car**2,)][0]
- 2
我們對list_of_list列表中第一個元素的第一個元素進行了一次算數(shù)運算,而且期間還對該算術(shù)運算進行了命名(但其作用域僅僅是在表達式的范圍內(nèi))。作為一種“優(yōu)化”,我們可以不用費心創(chuàng)建多于一個元素的列表就能開始運算了,因為我們結(jié)尾處用的索引為0,所以我們僅僅選擇的是第一個元素。:
從列表解析中高效地走下舞臺
- >>> [func for x in list_of_list[:1]
- ... for car in (x[0],)
- ... for func in (car+car**2,)][0]
- 2
高階函數(shù):currying
Python內(nèi)建的三個最常用的高階函數(shù)是:map()、reduce()和filter()。這三個函數(shù)所做的事情 —— 以及謂之為“高階”(higher-order)的原因 —— 是接受其它函數(shù)作為它們的(部分)參數(shù)。還有別的一些不屬于內(nèi)置的高階函數(shù),還會返回函數(shù)對象。
藉由函數(shù)對象在Python中具有首要地位, Python一直都有能讓其使用者構(gòu)造自己的高階函數(shù)的能力。舉個如下所示的小例子:
Python中一個簡單函數(shù)工廠(function factory)
- >>> def
- foo_factory():
- ...
- def
- foo():
- ...
- "Foo function from factory"
- ...
- return foo
- ...
- >>> f = foo_factory()
- >>> f()
- Foo function from factory
本系列文章的第二部分我討論過的Xoltar Toolkit中,有一組非常好用的高階函數(shù)。Xoltar的functional模塊中提供的絕大多數(shù)高階函數(shù)都是在其它各種不同的傳統(tǒng)型函數(shù)式編程語言中發(fā)展出來的高階函數(shù),其有用性已經(jīng)過多年的實踐驗證。
可能其中最著名、最有用和最重要的高階函數(shù)要數(shù)curry()了。函數(shù)curry()的名字取自于邏輯學(xué)家Haskell Curry,前文提及的一種編程語言也是用他姓名當(dāng)中的名字部分命名的。"currying"背后隱含的意思是,(幾乎)每一個函數(shù)都可以視為只帶一個參數(shù)的部分函數(shù)(partial function)。要使currying能夠用起來所需要做的就是讓函數(shù)本身的返回值也是個函數(shù),只不過所返回的函數(shù)“縮小了范圍”或者是“更加接近完整的函數(shù)”。這和我在第二部分中提到的閉包特別相似 —— 對經(jīng)過curry后的返回的后繼函數(shù)進行調(diào)用時一步一步“填入”最后計算所需的更多數(shù)據(jù)(附加到一個過程(procedure)之上的數(shù)據(jù))
現(xiàn)在讓我們先用Haskell中一個很簡單例子對curry進行講解,然后在Python中使用functional模塊重復(fù)展示一下這個簡單的例子:
在Haskell計算中使用Curry
- computation a b c d = (a + b^2+ c^3 + d^4)
- check = 1 + 2^2 + 3^3 + 5^4
- fillOne = computation 1
- -- specify "a"
- fillTwo = fillOne 2
- -- specify "b"
- fillThree = fillTwo 3
- -- specify "c"
- answer = fillThree 5
- -- specify "d"
- -- Result: check == answer == 657
現(xiàn)在使用Python:
在Python計算中使用Curry
- >>> from functional import curry
- >>> computation = lambda a,b,c,d: (a + b**2 + c**3 + d**4)
- >>> computation(1,2,3,5)
- 657
- >>> fillZero = curry(computation)
- >>> fillOne = fillZero(1)
- # specify "a"
- >>> fillTwo = fillOne(2)
- # specify "b"
- >>> fillThree = fillTwo(3)
- # specify "c"
- >>> answer = fillThree(5)
- # specify "d"
- >>> answer
- 657
第二部分中提到過的一個簡單的計稅程序的例子,當(dāng)時用的是閉包(這次使用curry()),可以用來進一步做個對比:
Python中curry后的計稅程序
- from functional import *
- taxcalc = lambda income,rate,deduct: (income-(deduct))*rate
- taxCurry = curry(taxcalc)
- taxCurry = taxCurry(50000)
- taxCurry = taxCurry(0.30)
- taxCurry = taxCurry(10000)
- print "Curried taxes due =",taxCurry
- print "Curried expression taxes due =", \
- curry(taxcalc)(50000)(0.30)(10000)
和使用閉包不同,我們需要以特定的順序(從左到右)對參數(shù)進行curry處理。當(dāng)要注意的是,functional模塊中還包含一個rcurry()類,能夠以相反的方向進行curry處理(從右到左)。
從一個層面講,其中的第二個print語句同簡單的同普通的taxcalc(50000,0.30,10000)函數(shù)調(diào)用相比只是個微小的拼寫方面的變化。但從另一個不同的層面講,它清晰地一個概念,那就是,每個函數(shù)都可以變換成僅僅帶有一個參數(shù)的函數(shù),這對于剛剛接觸這個概念的人來講,會有一種特別驚奇的感覺。
其它高階函數(shù)
除了上述的curry功能,functional模塊簡直就是一個很有意思的高階函數(shù)萬能口袋。此外,無論用還是不用functional模塊,編寫你自己的高階函數(shù)真的并不難。至少functional模塊中的那些高階函數(shù)為你提供了一些很值一看的思路。
它里面的其它高階函數(shù)在很大程度上感覺有點象是“增強”版本的標(biāo)準(zhǔn)高階函數(shù)map()、filter()和reduce()。這些函數(shù)的工作模式通常大致如此:將一個或多個函數(shù)以及一些列表作為參數(shù)接收進來,然后對這些列表參數(shù)運行它前面所接收到的函數(shù)。在這種工作模式方面,有非常大量很有意思也很有用的擺弄方法。還有一種模式是:拿到一組函數(shù)后,將這組函數(shù)的功能組合起來創(chuàng)建一個新函數(shù)。這種模式同樣也有大量的變化形式。下面讓我們看看functional模塊里到底還有哪些其它的高階函數(shù)。
sequential()和also()這兩個函數(shù)都是在一系列成分函數(shù)(component function)的基礎(chǔ)上創(chuàng)建一個新函數(shù)。然后這些成分函數(shù)可以通過使用相同的參數(shù)進行調(diào)用。兩者的主要區(qū)別就在于,sequential()需要一個單個的函數(shù)列表作為參數(shù),而also()接受的是一系列的多個參數(shù)。在多數(shù)情況下,對于函數(shù)的副作用而已這些會很有用,只是sequential()可以讓你隨意選擇將哪個函數(shù)的返回值作為組合起來后的新函數(shù)的返回值。
順序調(diào)用一系列函數(shù)(使用相同的參數(shù))
- >>> def a(x):
- ... print x,
- ... return "a"
- ...
- >>> def b(x):
- ... print x*2,
- ... return "b"
- ...
- >>> def c(x):
- ... print x*3,
- ... return "c"
- ...
- >>> r = also(a,b,c)
- >>> r
- <functional.sequential instance at 0xb86ac>
- >>> r(5)
- 5 10 15
- 'a'
- >>> sequential([a,b,c],main=c)('x')
- x xx xxx
- 'c'
isjoin()和conjoin()這兩個函數(shù)同equential()和also()在創(chuàng)建新函數(shù)并對參數(shù)進行多個成分函數(shù)的調(diào)用方面非常相似。只是disjoin()函數(shù)用來查詢成分函數(shù)中是否有一個函數(shù)的返回值(針對給定的參數(shù))為真;conjoin()函數(shù)用來查詢是否所有的成分函數(shù)的返回值都為真。在這些函數(shù)中只要條件允許就會使用邏輯短路,因此disjoin()函數(shù)可能不會出現(xiàn)某些副作用。joinfuncs()i同also()類似,但它返回的是由所有成分函數(shù)的返回值組成的一個元組(tuple),而不是選中的某個主函數(shù)。
前文所述的幾個函數(shù)讓你可以使用相同的參數(shù)對一系列函數(shù)進行調(diào)用,而any()、all()和 none_of()這三個讓你可以使用一個參數(shù)列表對同一個函數(shù)進行多次調(diào)用。在大的結(jié)構(gòu)方面,這些函數(shù)同內(nèi)置的map()、reduce()和filter()有點象。 但funtional模塊中的這三個高階函數(shù)中都是對一組返回值進行布爾(boolean)運算得到其返回值的。例如:
對一系列返回值的真、假情況進行判斷
- >>> from functional import *
- >>> isEven = lambda n: (n%2 == 0)
- >>> any([1,3,5,8], isEven)
- 1
- >>> any([1,3,5,7], isEven)
- 0
- >>> none_of([1,3,5,7], isEven)
- 1
- >>> all([2,4,6,8], isEven)
- 1
- >>> all([2,4,6,7], isEven)
- 0
有點數(shù)學(xué)基礎(chǔ)的人會對這個高階函數(shù)非常感興趣:iscompose(). 將多個函數(shù)進行合成(compostion)指的是,將一個函數(shù)的返回值同下個函數(shù)的輸入“鏈接到一起”。對多個函數(shù)進行合成的程序員需要負責(zé)保證函數(shù)間的輸入和輸出是相互匹配的,不過這個條件無論是程序員在何時想使用返回值時都是需要滿足的。舉個簡單的例子和闡明這一點:
創(chuàng)建合成函數(shù)
- >>> def minus7(n): return n-7
- ...
- >>> def times3(n): return n*3
- ...
- >>> minus7(10)
- 3
- >>> minustimes = compose(times3,minus7)
- >>> minustimes(10)
- 9
- >>> times3(minus7(10))
- 9
- >>> timesminus = compose(minus7,times3)
- >>> timesminus(10)
- 23
- >>> minus7(times3(10))
- 23
后會有期
衷心希望我對高階函數(shù)的思考能夠引起讀者的興趣。無論如何,請動手試一試。試著編寫一些你自己的高階函數(shù);一些可能很有用,很強大。告訴我它如何運行;或許這個系列之后的章節(jié)會討論讀者不斷提供的新觀點,新想法。
原文鏈接:http://www.oschina.net/translate/python-functional-programming-part3