英文原文:Charming Python: Functional programming in Python, Part 2,翻譯:開源中國
摘要: 本專欄繼續讓David對Python中的函數語言程式設計(FP)進行介紹。讀完本文,可以享受到使用不同的程式設計範型(paradigm)解決問題所帶來的樂趣。David在本文中對FP中的多箇中級和高階概念進行了詳細的講解。
一個物件就是附有若干過程(procedure)的一段資料。。。一個閉包(closure)就是附有一段資料的一個過程(procedure)。
在我講解函數語言程式設計的上一篇文章,第一部分,中,我介紹了FP中的一些基本概念。 本文將更加深入的對這個內容十分豐富的概念領域進行探討。在我們探討的大部分內容中,Bryn Keller的”Xoltar Toolkit”為我們提供一些非常有價值的幫助作用。Keller將FP中的許多強項集中到了一個很棒且很小的模組中,他在這個模組中用純Python程式碼實現了這些強項。除了functional模組外,Xoltar Toolkit還包含了一個延遲(lazy)模組,對“僅在需要時”才進行求值提供了支援。許多傳統的函式式語言中也都具有延遲求值的手段,這樣,使用Xoltar Toolkit中的這些元件,你就可以做到使用象Haskell這樣的函式式語言能夠做到的大部分事情了。
繫結(Binding)
有心的讀者會記得,我在第一部分中所述的函式式技術中指出過Python的一個侷限。具體講,就是Python中沒有任何手段禁止對用來指代函式式表示式的名字進行重新繫結。 在FP中,名字一般是理解為對比較長的表示式的簡稱,但這裡面隱含了一個諾言,就是“同一個表示式總是具有同一個值”。如果對用來指代的名字重新進行繫結,就會違背這個諾言。例如, 假如我們如以下所示,定義了一些要用在函式式程式中的簡記表示式:
Python中由於重新繫結而引起問題的FP程式設計片段
1 2 3 4 5 6 7 8 |
>>> 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))在兩個不同的點求得的值卻不相同, 儘管在該表示式的引數中根本沒有使用任何可變的(mutable)變數。
幸運的是, functional模組提供了一個叫做Bindings(由鄙人向Keller進行的提議,proposed to Keller by yours truly)的類,可以用來避免這種重新繫結(至少可以避免意外的重新繫結,Python並不阻止任何拿定主意就是要打破規則的程式設計師)。儘管要用Bindings類就需要使用一些額外的語法,但這麼做就能讓這種事故不太容易發生。 Keller在functional模組裡給出的例子中,有個Bindings的例項名字叫做let(我推測這麼叫是為了仿照ML族語言中的let關鍵字)。例如,我們可以這麼做:
Python中對重新繫結進行監視後的FP程式設計片段
1 2 3 4 5 6 7 8 9 10 11 |
>>> 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”異常,但發出這些異常這件事,就能夠避免產生這一大類的問題。
functional模組隨同Bindings一起還提供了一個叫做namespace的函式,這個函式從Bindings例項中弄出了一個名稱空間 (實際就是個字典) 。如果你想計算一個表示式,而該表示式是在定義於一個Bindings中的一個(不可變)名稱空間中時,這個函式就可以很方便地拿來使用。Python的eval()函式允許在名稱空間中進行求值。舉個例子就能說明這一切:
Python中使用不可變名稱空間的FP程式設計片段
1 2 3 4 5 6 7 8 9 10 11 |
>>> 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 |
FP中有一個特別有引人關注的概念叫做閉包。實際上,閉包充分引起了很多程式設計師的關注,即使通常意義上的非函數語言程式設計語言,比如Perl和Ruby,都包含了閉包這一特性。此外,Python 2.1 目前一定會新增上詞法域(lexical scoping), 這樣一來就提供的閉包的絕大多數功能。
那麼,閉包到底是什麼?Steve Majewski最近在Python新聞組中對這個概念的特性提出了一個準確的描述:
就是說,閉包就象是FP的Jekyll,OOP(物件導向程式設計)的 Hyde (或者可能是將這兩個角色互換)(譯者注:Jekyll和Hyde是一部小說中的兩個人物). 和象物件例項類似,閉包是一種把一堆資料和一些功能打包一起進行傳遞的手段。
先讓我們後退一小步,看看物件和閉包都能解決一些什麼樣的問題,然後再看看在兩樣都不用的情況下這些問題是如何得到解決的。函式返回的值通常是由它在計算過程中使用的上下文決定的。最常見可能也是最顯然的指定該上下文的方式就是給函式傳遞一些引數,讓該函式對這些引數進行一些運算。但有時候在引數的“背景”(background)和“前景”(foreground)兩者之間也有一種自然的區分,也就是說,函式在某特定時刻正在做什麼和函式“被配置”為處於多種可能的呼叫情況之下這兩者之間有不同之處。
在集中處理前景的同時,有多種方式進行背景處理。一種就是“忍辱負重”,每次呼叫時都將函式需要的每個引數傳遞給函式。這通常就相對於在函式呼叫鏈中不斷的將很多值(或者是一個具有很多欄位的資料結構)傳上傳下,就是因為在鏈中的某個地方可能會用到這些值。下面舉個簡單的例子:
用了貨船變數的Python程式碼片段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
>>> 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 |
在上述的貨船變數例子中,函式b()中的變數n毫無意義,就只是為了傳遞給函式c()。另一種辦法是使用全域性變數:
使用全域性變數的Python程式碼片段
1 2 3 4 5 6 7 8 9 10 |
>>> 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只要你想呼叫ddN()就可以直接使用,就不需要顯式地傳遞這個全域性背景“上下文”了。有個稍微更加Python化的技巧,可以用來在定義函式時,通過使用預設引數將一個變數“凍結”到該函式中:
使用凍結變數的Python程式碼片段
1 2 3 4 5 6 7 8 9 |
>>> 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 |
我們凍結的變數實質上就是個閉包。我們將一些資料“附加”到了addN()函式之上。對於一個完整的閉包而言,在函式addN()定義時所出現的資料,應該在該函式被呼叫時也可以拿到。然而,本例中(以及更多更健壯的例子中),使用預設引數讓足夠的資料可用非常簡單。函式addN()不再使用的變數因而對計算結構捕獲產生絲毫影響。
現在讓我們再看一個用OOP的方式解決一個稍微更加現實的問題。今年到了這個時候,讓我想起了頗具“面試”風格的計稅程式,先收集一些資料,資料不一定有什麼特別的順序,最後使用所有這些資料進行一個計算。讓我們為這種情況些個簡化版本的程式:
Python風格的計稅類/例項
1 2 3 4 5 6 7 |
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類 (或者更準確的講,在它的例項中),我們先收集了一些資料,資料的順序隨心所欲,然後所有需要的資料收集完成後,我們可以呼叫這個物件的一個方法,對這堆資料進行計算。所有的一切都呆在一個例項中,而且,不同的例項可以擁有一堆不同的資料。能夠建立多個例項,而多個例項僅僅是資料不同,這通過“全域性變數”和“凍結變數”這兩種方法是無法辦到的。”貨船”方法能夠做到這一點,但從那個展開的例子中我們能夠看出,它可能不得不在開始時就傳遞多個數值。討論到這裡,注意到OOP風格的訊息傳遞方式可能會如何來解決這一問題會非常有趣(Smalltalk或者Self與此類似,我所用過的好幾種xBase的變種OOP語言也是類似的):
Smalltalk風格的(Python) 計稅程式
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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可以讓我們將每個方法呼叫的結果當作“當前”物件進行處理。這和FP中的閉包方式有些相似。
通過使用Xoltar toolkit,我們可以生成完整的閉包,能夠將資料和函式結合起來,獲得我們所需的特性;另外還可以讓多個閉包(以前成為物件)包含不同的資料:
Python的函式式風格的計稅程式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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)))() |
我們所定義的每個閉包函式可以獲取函式定義範圍內的任意值,然後將這些值繫結到改函式物件的全域性範圍之中。然而,一個函式的全域性範圍並不一定就是真正的模組全域性範圍,也和不同的閉包的“全域性”範圍不相同。閉包就是“將資料帶”在了身邊。
在我們的例子中,我們利用了一些特殊的函式把特定的繫結限定到了一個閉包作用範圍之中(income, deduct, rate)。要想修改設計,將任意的繫結限定在閉包之中,也非常簡單。只是為了好玩,在本例子中我們也使用了兩種稍微不同的函式式風格。第一種風格連續將多個值繫結到了閉包的作用範圍;通過允許taxFP成為可變的變數,這些“新增繫結”的程式碼行可以任意順序出現。然而,如果我們想要使用tax_with_Income這樣的不可變名字,我們就需要以特定的順序來安排這幾行進行繫結的程式碼,將靠前的繫結結果傳遞給下一個繫結。無論在哪種情況下,在全部所需資料都繫結進閉包範圍之後,我們就可以呼叫“種子”(seeded)方法了。
第二種風格在我看來,更象是Lisp(那些括號最象了)。除去美學問題,這第二種風格有兩點值得注意。第一點就是完全避免了名字繫結,變成了一個單個的表示式,連語句都沒有使用(關於為什麼不使用語句很重要,請參見 P第一部分)。
第二點是閉包的“Lips”風格的用法和前文給出的“Smalltalk”風格的資訊傳遞何其相似。實際上兩者都在呼叫taxdue()函式/方法的過程中積累了所有值(如果以這種原始的方式拿不到正確的資料,兩種方式都會出錯)。“Smalltalk”風格的方法中每一步傳遞的是一個物件,而“Lisp”風格的方法中傳遞是持續進行的。 但實際上,函數語言程式設計和麵向物件式程式設計兩者旗鼓相當。
在本文中,我們幹掉了函數語言程式設計領域中更多的內容。剩下的要比以前(本小節的題目是個小玩笑;很不幸,這裡還沒有解釋過尾遞迴的概念)少多了(或者可以證明也簡單多了?)。閱讀functional模組中的原始碼是繼續探索FP中大量概念的一種非常好的方法。該模組中的註釋很完備,在註釋裡為模組中的大多數方法/類提供了相關的例子。其中有很多簡化性的元函式(meta-function)本專欄裡並沒有討論到的,使用這些元函式可以大大簡化對其它函式的結合(combination)和互動(interaction )的處理。對於想繼續探索函式式範型的Python程式設計師而言,這些絕對值得好好看看。
可愛的 Python : Python中的函數語言程式設計,第三部分