可愛的Python函數(shù)式編程(二)
摘要:本專欄繼續(xù)讓David對Python中的函數(shù)式編程(FP)進行介紹。讀完本文,可以享受到使用不同的編程范型(paradigm)解決問題所帶來的樂趣。David在本文中對FP中的多個中級和高級概念進行了詳細的講解。
一個對象就是附有若干過程(procedure)的一段數(shù)據(jù)。。。一個閉包(closure)就是附有一段數(shù)據(jù)的一個過程(procedure)。
在我講解函數(shù)式編程的上一篇文章,第一部分,中,我介紹了FP中的一些基本概念。 本文將更加深入的對這個內(nèi)容十分豐富的概念領域進行探討。在我們探討的大部分內(nèi)容中,Bryn Keller的"Xoltar Toolkit"為我們提供一些非常有價值的幫助作用。Keller將FP中的許多強項集中到了一個很棒且很小的模塊中,他在這個模塊中用純Python代碼實現(xiàn)了這些強項。除了functional模塊外,Xoltar Toolkit還包含了一個延遲(lazy)模塊,對“僅在需要時”才進行求值提供了支持。許多傳統(tǒng)的函數(shù)式語言中也都具有延遲求值的手段,這樣,使用Xoltar Toolkit中的這些組件,你就可以做到使用象Haskell這樣的函數(shù)式語言能夠做到的大部分事情了。
綁定(Binding)
有心的讀者會記得,我在第一部分中所述的函數(shù)式技術中指出過Python的一個局限。具體講,就是Python中沒有任何手段禁止對用來指代函數(shù)式表達式的名字進行重新綁定。 在FP中,名字一般是理解為對比較長的表達式的簡稱,但這里面隱含了一個諾言,就是“同一個表達式總是具有同一個值”。如果對用來指代的名字重新進行綁定,就會違背這個諾言。例如, 假如我們?nèi)缫韵滤?,定義了一些要用在函數(shù)式程序中的簡記表達式:
Python中由于重新綁定而引起問題的FP編程片段
- >>> car = lambda lst: lst[0]
- >>> cdr = lambda lst: lst[1:]
- >>> sum2 = lambda lst: car(lst)+car(cdr(lst))
- >>> sum2(range(10))
- 1
- >>> car = lambda lst: lst[2]
- >>> sum2(range(10))
- 5
非常不幸,程序中完全相同的表達式sum2(range(10))在兩個不同的點求得的值卻不相同, 盡管在該表達式的參數(shù)中根本沒有使用任何可變的(mutable)變量。
幸運的是, functional模塊提供了一個叫做Bindings(由鄙人向Keller進行的提議,proposed to Keller by yours truly)的類,可以用來避免這種重新綁定(至少可以避免意外的重新綁定,Python并不阻止任何拿定主意就是要打破規(guī)則的程序員)。盡管要用Bindings類就需要使用一些額外的語法,但這么做就能讓這種事故不太容易發(fā)生。 Keller在functional模塊里給出的例子中,有個Bindings的實例名字叫做let(我推測這么叫是為了仿照ML族語言中的let關鍵字)。例如,我們可以這么做:
Python中對重新綁定進行監(jiān)視后的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.
- >>> car(range(10))
- 0
顯而易見,在真正的程序中應該去做一些事情,捕獲這種"BindingError"異常,但發(fā)出這些異常這件事,就能夠避免產(chǎn)生這一大類的問題。
functional模塊隨同Bindings一起還提供了一個叫做namespace的函數(shù),這個函數(shù)從Bindings實例中弄出了一個命名空間 (實際就是個字典) 。如果你想計算一個表達式,而該表達式是在定義于一個Bindings中的一個(不可變)命名空間中時,這個函數(shù)就可以很方便地拿來使用。Python的eval()函數(shù)允許在命名空間中進行求值。舉個例子就能說明這一切:
Python中使用不可變命名空間的FP編程片段
- >>> let = Bindings() # "Real world" function names
- >>> let.r10 = range(10)
- >>> let.car = lambda lst: lst[0]
- >>> let.cdr = lambda lst: lst[1:]
- >>> eval('car(r10)+car(cdr(r10))', namespace(let))
- >>> inv = Bindings() # "Inverted list" function names
- >>> inv.r10 = let.r10
- >>> inv.car = lambda lst: lst[-1]
- >>> inv.cdr = lambda lst: lst[:-1]
- >>> eval('car(r10)+car(cdr(r10))', namespace(inv))
- 17
閉包(Closure)
FP中有一個特別有引人關注的概念叫做閉包。實際上,閉包充分引起了很多程序員的關注,即使通常意義上的非函數(shù)式編程語言,比如Perl和Ruby,都包含了閉包這一特性。此外,Python 2.1 目前一定會添加上詞法域(lexical scoping), 這樣一來就提供的閉包的絕大多數(shù)功能。
那么,閉包到底是什么?Steve Majewski最近在Python新聞組中對這個概念的特性提出了一個準確的描述:
就是說,閉包就象是FP的Jekyll,OOP(面向?qū)ο缶幊蹋┑?Hyde (或者可能是將這兩個角色互換)(譯者注:Jekyll和Hyde是一部小說中的兩個人物). 和象對象實例類似,閉包是一種把一堆數(shù)據(jù)和一些功能打包一起進行傳遞的手段。
先讓我們后退一小步,看看對象和閉包都能解決一些什么樣的問題,然后再看看在兩樣都不用的情況下這些問題是如何得到解決的。函數(shù)返回的值通常是由它在計算過程中使用的上下文決定的。最常見可能也是最顯然的指定該上下文的方式就是給函數(shù)傳遞一些參數(shù),讓該函數(shù)對這些參數(shù)進行一些運算。但有時候在參數(shù)的“背景”(background)和“前景”(foreground)兩者之間也有一種自然的區(qū)分,也就是說,函數(shù)在某特定時刻正在做什么和函數(shù)“被配置”為處于多種可能的調(diào)用情況之下這兩者之間有不同之處。
在集中處理前景的同時,有多種方式進行背景處理。一種就是“忍辱負重”,每次調(diào)用時都將函數(shù)需要的每個參數(shù)傳遞給函數(shù)。這通常就相對于在函數(shù)調(diào)用鏈中不斷的將很多值(或者是一個具有很多字段的數(shù)據(jù)結(jié)構)傳上傳下,就是因為在鏈中的某個地方可能會用到這些值。下面舉個簡單的例子:
用了貨船變量的Python代碼片段
- >>> def a(n):
- ... add7 = b(n)
- ... return add7
- ...
- >>> def b(n):
- ... i = 7
- ... j = c(i,n)
- ... return j
- ...
- >>> def c(i,n):
- ... return i+n
- ...
- >>> a(10) # Pass cargo value for use downstream
- 17
在上述的貨船變量例子中,函數(shù)b()中的變量n毫無意義,就只是為了傳遞給函數(shù)c()。另一種辦法是使用全局變量:
使用全局變量的Python代碼片段
- >>> N = 10
- >>> def addN(i):
- ... global N
- ... return i+N
- ...
- >>> addN(7) # Add global N to argument
- 17
- >>> N = 20
- >>> addN(6) # Add global N to argument
- 26
全局變量N只要你想調(diào)用ddN()就可以直接使用,就不需要顯式地傳遞這個全局背景“上下文”了。有個稍微更加Python化的技巧,可以用來在定義函數(shù)時,通過使用缺省參數(shù)將一個變量“凍結(jié)”到該函數(shù)中:
使用凍結(jié)變量的Python代碼片段
- >>> N = 10
- >>> def addN(i, n=N):
- ... return i+n
- ...
- >>> addN(5) # Add 10
- 15
- >>> N = 20
- >>> addN(6) # Add 10 (current N doesn't matter)
- 16
我們凍結(jié)的變量實質(zhì)上就是個閉包。我們將一些數(shù)據(jù)“附加”到了addN()函數(shù)之上。對于一個完整的閉包而言,在函數(shù)addN()定義時所出現(xiàn)的數(shù)據(jù),應該在該函數(shù)被調(diào)用時也可以拿到。然而,本例中(以及更多更健壯的例子中),使用缺省參數(shù)讓足夠的數(shù)據(jù)可用非常簡單。函數(shù)addN()不再使用的變量因而對計算結(jié)構捕獲產(chǎn)生絲毫影響。
現(xiàn)在讓我們再看一個用OOP的方式解決一個稍微更加現(xiàn)實的問題。今年到了這個時候,讓我想起了頗具“面試”風格的計稅程序,先收集一些數(shù)據(jù),數(shù)據(jù)不一定有什么特別的順序,最后使用所有這些數(shù)據(jù)進行一個計算。讓我們?yōu)檫@種情況些個簡化版本的程序:
Python風格的計稅類/實例
- class TaxCalc:
- def taxdue(self):return (self.income-self.deduct)*self.rate
- taxclass = TaxCalc()
- taxclass.income = 50000
- taxclass.rate = 0.30
- taxclass.deduct = 10000
- print"Pythonic OOP taxes due =", taxclass.taxdue()
在我們的TaxCalc類 (或者更準確的講,在它的實例中),我們先收集了一些數(shù)據(jù),數(shù)據(jù)的順序隨心所欲,然后所有需要的數(shù)據(jù)收集完成后,我們可以調(diào)用這個對象的一個方法,對這堆數(shù)據(jù)進行計算。所有的一切都呆在一個實例中,而且,不同的實例可以擁有一堆不同的數(shù)據(jù)。能夠創(chuàng)建多個實例,而多個實例僅僅是數(shù)據(jù)不同,這通過“全局變量”和“凍結(jié)變量”這兩種方法是無法辦到的。"貨船"方法能夠做到這一點,但從那個展開的例子中我們能夠看出,它可能不得不在開始時就傳遞多個數(shù)值。討論到這里,注意到OOP風格的消息傳遞方式可能會如何來解決這一問題會非常有趣(Smalltalk或者Self與此類似,我所用過的好幾種xBase的變種OOP語言也是類似的):
Smalltalk風格的(Python) 計稅程序
- class TaxCalc:
- def taxdue(self):return (self.income-self.deduct)*self.rate
- def setIncome(self,income):
- self.income = income
- return self
- def setDeduct(self,deduct):
- self.deduct = deduct
- return self
- def setRate(self,rate):
- self.rate = rate
- return self
- print"Smalltalk-style taxes due =", \
- TaxCalc().setIncome(50000).setRate(0.30).setDeduct(10000).taxdue()
每個"setter"方法都返回self可以讓我們將每個方法調(diào)用的結(jié)果當作“當前”對象進行處理。這和FP中的閉包方式有些相似。
通過使用Xoltar toolkit,我們可以生成完整的閉包,能夠?qū)?shù)據(jù)和函數(shù)結(jié)合起來,獲得我們所需的特性;另外還可以讓多個閉包(以前成為對象)包含不同的數(shù)據(jù):
Python的函數(shù)式風格的計稅程序
- from functional import *
- taxdue = lambda: (income-deduct)*rate
- incomeClosure = lambda income,taxdue: closure(taxdue)
- deductClosure = lambda deduct,taxdue: closure(taxdue)
- rateClosure = lambda rate,taxdue: closure(taxdue)
- taxFP = taxdue
- taxFP = incomeClosure(50000,taxFP)
- taxFP = rateClosure(0.30,taxFP)
- taxFP = deductClosure(10000,taxFP)
- print"Functional taxes due =",taxFP()
- print"Lisp-style taxes due =", \
- incomeClosure(50000,
- rateClosure(0.30,
- deductClosure(10000, taxdue)))()
我們所定義的每個閉包函數(shù)可以獲取函數(shù)定義范圍內(nèi)的任意值,然后將這些值綁定到改函數(shù)對象的全局范圍之中。然而,一個函數(shù)的全局范圍并不一定就是真正的模塊全局范圍,也和不同的閉包的“全局”范圍不相同。閉包就是“將數(shù)據(jù)帶”在了身邊。
在我們的例子中,我們利用了一些特殊的函數(shù)把特定的綁定限定到了一個閉包作用范圍之中(income, deduct, rate)。要想修改設計,將任意的綁定限定在閉包之中,也非常簡單。只是為了好玩,在本例子中我們也使用了兩種稍微不同的函數(shù)式風格。第一種風格連續(xù)將多個值綁定到了閉包的作用范圍;通過允許taxFP成為可變的變量,這些“添加綁定”的代碼行可以任意順序出現(xiàn)。然而,如果我們想要使用tax_with_Income這樣的不可變名字,我們就需要以特定的順序來安排這幾行進行綁定的代碼,將靠前的綁定結(jié)果傳遞給下一個綁定。無論在哪種情況下,在全部所需數(shù)據(jù)都綁定進閉包范圍之后,我們就可以調(diào)用“種子”(seeded)方法了。
第二種風格在我看來,更象是Lisp(那些括號最象了)。除去美學問題,這第二種風格有兩點值得注意。第一點就是完全避免了名字綁定,變成了一個單個的表達式,連語句都沒有使用(關于為什么不使用語句很重要,請參見 P第一部分)。
第二點是閉包的“Lips”風格的用法和前文給出的“Smalltalk”風格的信息傳遞何其相似。實際上兩者都在調(diào)用taxdue()函數(shù)/方法的過程中積累了所有值(如果以這種原始的方式拿不到正確的數(shù)據(jù),兩種方式都會出錯)。“Smalltalk”風格的方法中每一步傳遞的是一個對象,而“Lisp”風格的方法中傳遞是持續(xù)進行的。 但實際上,函數(shù)式編程和面向?qū)ο笫骄幊虄烧咂旃南喈敗?/p>
尾遞歸
在本文中,我們干掉了函數(shù)式編程領域中更多的內(nèi)容。剩下的要比以前(本小節(jié)的題目是個小玩笑;很不幸,這里還沒有解釋過尾遞歸的概念)少多了(或者可以證明也簡單多了?)。閱讀functional模塊中的源代碼是繼續(xù)探索FP中大量概念的一種非常好的方法。該模塊中的注釋很完備,在注釋里為模塊中的大多數(shù)方法/類提供了相關的例子。其中有很多簡化性的元函數(shù)(meta-function)本專欄里并沒有討論到的,使用這些元函數(shù)可以大大簡化對其它函數(shù)的結(jié)合(combination)和交互(interaction )的處理。對于想繼續(xù)探索函數(shù)式范型的Python程序員而言,這些絕對值得好好看看。
原文鏈接:http://www.oschina.net/translate/python-functional-programming-part2