摘要:雖然人們總把Python當作過程化的,面向對象的語言,但是他實際上包含了函數化編程中,你需要的任何東西。這篇文章主要討論函數化編程的一般概念,並說明用Python來函數化編程的技術。 我們最好從艱難的問題開始出發:“到底什么是函數化編程呢?”其中一個答案可能是這樣的,函數化編程就是你在使用Lisp這樣的語言時所做的(還有Scheme,Haskell,ML,OCAML,Mercury,Erlang和其他一些語言)。這是一個保險的回答,但是它解釋得並不清晰。不幸的是對於什么是函數化編程,很難能有一個協調一致的定義,即使是從函數化變成本身出發,也很難說明。這點倒很像盲人摸象。不過,把它拿來和命令式編程(imperative programming)做比較也不錯(命令式編程就像你在用C,Pascal,C++,Java,Perl,Awk,TCL和很多其他類似語言時所做的,至少大部分一樣 )。 |
我個人粗略總結了一下,認為函數式編程至少應該具有下列幾點中的多個特點。在謂之為函數式的語言中,要做到這些就比較容易,但要做到其它一些事情不是很難就是完全不可能:
函數式編程的倡導者們認為,所有這些特性都有助於更快地編寫出更多更簡潔並且更不容易出Bug的代碼。而且,計算機科學、邏輯學和數學這三個領域中的高級理論家發現,函數式編程語言和程序的形式化特性在證明起來比命令式編程語言和程序要簡單很多。 |
Python內在的函數式功能自Python 1.0起,Python就已具有了以上所列中的絕大多數特點。但是就象Python所具有的大多數特性一樣,這些特點出現在了一種混合了各種特性的語言中。 和Python的OOP(面向對象編程) 特性非常象,你想用多少就用多少,剩下的都可以不管(直到你隨后需要用到它們為止)。在Python 2.0中,加入了列表解析(list comprehensions)這個非常好用的”語法糖“。 盡管列表解析沒有添加什么新功能,但它讓很多舊功能看起來好了不少。 Python中函數式編程的基本要素包括functionsmap()、reduce()、filter()和lambda算子(operator)。 在Python 1.x中,apply()函數也可以非常方便地拿來將一個函數的列表返回值直接用於另外一個函數。Python 2.0為此提供了一個改進后的語法。可能有點讓人驚奇,使用如此之少的函數(以及基本的算子)幾乎就足以寫出任何Python程序了;更加特別的是,幾乎用不着什么執行流程控制語句。 所有(if,elif,else,assert,try,except,finally,for,break,continue,while,def)這些都都能通過僅僅使用函數式編程中的函數和算子就能以函數式編程的風格處理好。盡管真正地在程序中完全排除使用所有流程控制命令可能只在想參加”Python混亂編程“大賽(可將Python代碼寫得跟Lisp代碼非常象)時才有意義,但這對理解函數式編程如何通過函數和遞歸表達流程控制很有價值。 |
剔除流程控制語句剔除練習首先要考慮的第一件事是,實際上,Python會對布爾表達式求值進行“短路”處理。這就為我們提供了一個if/elif/else分支語句的表達式版(假設每個分支只調用一個函數,不是這種情況時也很容易組織成重新安排成這種情況)。 這里給出怎么做: 對Python中的條件調用進行短路處理
我們的表達式版本的條件調用看上去可能不算什么,更象是個小把戲;然而,如果我們注意到lambda算子必須返回一個表達式,這就更值得關注了。既然如我們所示,表達式能夠通過短路包含一個條件判斷,那么,lambda表達式就是個完全通用的表達條件判斷返回值的手段了。我們來一個例子: Python中短路的Lambda
|
將函數作為具有首要地位的對象 前面的例子已經表明了Python中函數具有首要地位,但有點委婉。當我們用lambda操作創建一個函數對象時, 我們所得到的東西是完全通用的。就其本質而言,我們可以將我們的對象同名字"pr"和"namenum"綁定到一起, 以完全相同的方式,我們也也完全可以將數字23或者字符串"spam" 同這些名字綁定到一起。但是,就象我們可以無需將其綁定到任何名字之上就能直接使用數字23(也就是說,它可以用作函數的參數)一樣,我們也可以直接使用我們使用lambda創建的函數對象,而無需將其綁定到任何名字之上。在Python中,函數就是另外一種我們能夠就像某種處理的值。 我們對具有首要地位的對象做的比較多的事情就是,將它們作為參數傳遞給函數式編程固有的函數map()、reduce()和filter()。這三個函數接受的第一個參數都是一個函數對象。
我們經常也會把函數對象傳遞給我們自己定義的函數,不過一般情況下這些自定義的函數就是前文提及的內建函數的某種形式的組合。 通過組合使用這三種函數式編程內建的函數, 能夠實現范圍驚人的“執行流程”操作(全都不用語句,僅僅使用表達式實現)。 |
Python中的函數式循環替換循環語言和條件狀態語言塊同樣簡單。for可以直接翻譯成map()函數。正如我們的條件執行,我們會需要簡化語句塊成簡單的函數調用(我們正在接近通常能做的): 替換循環
Map-based 動作序列
翻譯while會稍稍復雜一些,但仍然可以直接地完成: Python中的函數式"while"循環
|
在翻譯while循環時,我們仍然需要使用while_block()函數,這個函數本身里面可以包含語句而不是僅僅包含表達式。但我們可能還能夠對這個函數再進行更進一步的剔除過程(就像前面模版中的對if/else進行短路處理一樣)。 還有,<cond>很難對普通的測試有什么用,比如while myvar==7,既然循環體(在設計上)不能對任何變量的值進行修改(當然,在while_block()中可以修改全局變量)。有一種方法可以用來為
while_block()添加更有用的條件判斷,讓
while_block()返回一個有意義的值,然后將這個返回值同循環結束條件進行比較。現在應該來看一個剔除其中語句的具體例子了:
Python中'echo'循環
|
避免副作用在做完這些沒有非常明智的理由陳述,並把晦澀的嵌套表達式代替他們之后,一個很自然的問題是“為什么要這樣做?!” 我描述的函數式編程在Python中都實現了。但是最重要的特性和一個有具體用處——就是避免副作用(或至少它們阻止如monads的特殊區域)。程序錯誤的大部分——並且這些問題驅使程序員去debug——出現是因為在程序的運行中變量獲取了非期望的值。函數式編程簡單地通過從不給變量賦值而繞過了這個問題。 |
現在讓我們看一段非常普通的命令式代碼。這段代碼的目的是打印出乘積大於25的一對一對數字所組成的一個列表。組成每對數字的每一個數字都是取自另外的兩個列表。這種事情和很多程序員在他們的編程中經常做的一些事情比較相似。命令式的解決方式有可能就象下面這樣: 命令式的"打印大乘積"的Python代碼
這個項目足夠小了,好像沒有地方會出什么差錯。但有可能在這段代碼中我們會嵌入一些同時完成其它任務的代碼。用"more stuff"(其它代碼)注釋掉的部分,就是有可能存在導致出現bug的副作用的地方。在那三部分的任何一點上,變量sxs、ys、bigmuls、x、y都有可能在這段按照理想情況簡化后的代碼中取得一個出人意料的值。還有,這段代碼執行完后,后繼代碼有可能需要也有可能不需要對所有這些變量中的值有所預期。顯而易見,將這段代碼封裝到函數/實例中,小心處理變量的作用范圍,就能夠避免這種類型的錯誤。你也可以總是將使用完畢的變量del掉。但在實踐中,這里指出的這種類型的錯誤很常見。 |
以一種函數式的途徑一舉消除這些副作用所產生的錯誤,這樣就達到了我們的目的。一種可能的代碼如下: 以函數式途徑達到我們的目的
|
這個函數式例子的真正優點在於:在函數中絕對沒有改變變量的值。這樣就不可能在之后的代碼(或者從之前的代碼)中產生不可預期的副作用。顯然,在函數中沒有副作用,並不能保證代碼的正確性,但它仍然是一個優點。無論如何請注意,Python(不像很多其它的函數式語言)不會阻止名字bigmuls,combine和dupelms的再次綁定。如果combine()運行在之后的程序中意味着有所不同時,所有的預測都會失效。你可能會需要新建一個單例類來包含這個不變的綁定(也就是說,s.bigmuls之類的);但是這一例並沒有空間來做這些。 一個明顯值得注意的是,我們特定的目標是定制Python 2的一些特性。而不是命令式的或函數式編程的例子,最好的(也是函數式的)方法是:
|
結束語我已經列出了把每一個Python控制流替換成一個相等的函數式代碼的方法(在程序中減少副作用)。高效翻譯一個特定的程序需要一些額外的思考,但我們已經看出內置的函數式功能是全面且完善的。在接下來的文章里,我們會看到更多函數式編程的高級技巧;並且希望我們接下來能夠摸索到函數式編程風格的更多優點和缺點。
|