可愛的 Python : Python中的函數語言程式設計,第三部分

發表於2013-03-07

英文原文:Charming Python: Functional programming in Python, Part 3,翻譯:開源中國

摘要:  作者David Mertz在其文章《可愛的Python:“Python中的函數語言程式設計”》中的第一部分第二部分中觸及了函數語言程式設計的大量基本概念。本文中他將繼續前面的討論,解釋函數語言程式設計的其它功能,如currying和Xoltar Toolkit中的其它一些高階函式。

表示式繫結

有一位從不滿足於解決部分問題讀者,名叫Richard Davies,提出了一個問題,問是否可以將所有的繫結全部都轉移到一個單個的表示式之中。首先讓我們簡單看看,我們為什麼想這麼做,然後再看看由comp.lang.python中的一位朋友提供的一種異常優雅地寫表示式的方式。

讓我們回想一下功能模組的繫結類。使用該類的特性,我們可以確認在一個給定的範圍塊內,一個特定的名字僅僅代表了一個唯一的事物。
具有重新繫結向導的 Python 函數語言程式設計(FP)

繫結類在一個模組或者一個功能定義範圍內做這些我們希望的事情,但是沒有辦法在一條表示式內使之工作。然而在ML家族語言(譯者注:ML是一種通用的函數語言程式設計語言),在一條表示式內建立繫結是很自然的事。
Haskell 命名繫結表示式

Greg Ewing 發現用Python的list概念實現同樣的效果是有可能的;甚至我們可以用幾乎與Haskell語法一樣乾淨的方式做到。
Python 2.0+ 命名繫結表示式

在列表解析(list comprehension)中將表示式放入一個單項元素(a single-item tuple)中的這個小技巧,並不能為使用帶有表示式級繫結的高階函式提供任何思路。要使用這樣的高階函式,還是需要使用塊級(block-level)繫結,就象以下所示:
Python中的使用塊級繫結的’map()’

這樣真不錯,但如果我們想使用函式map(),那麼其中的繫結範圍可能會比我們想要的更寬一些。然而,我們可以做到的,哄騙列表解析讓它替我們做名字繫結,即使其中的列表並不是我們最終想要得到的列表的情況下也沒問題:
從Python的列表解析中“走下舞臺”

我們對list_of_list列表中第一個元素的第一個元素進行了一次算數運算,而且期間還對該算術運算進行了命名(但其作用域僅僅是在表示式的範圍內)。作為一種“優化”,我們可以不用費心建立多於一個元素的列表就能開始運算了,因為我們結尾處用的索引為0,所以我們僅僅選擇的是第一個元素。:
從列表解析中高效地走下舞臺

高階函式:currying

Python內建的三個最常用的高階函式是:map()、reduce()和filter()。這三個函式所做的事情 —— 以及謂之為“高階”(higher-order)的原因 —— 是接受其它函式作為它們的(部分)引數。還有別的一些不屬於內建的高階函式,還會返回函式物件。
藉由函式物件在Python中具有首要地位, Python一直都有能讓其使用者構造自己的高階函式的能力。舉個如下所示的小例子:
Python中一個簡單函式工廠(function factory)

本系列文章的第二部分我討論過的Xoltar Toolkit中,有一組非常好用的高階函式。Xoltar的functional模組中提供的絕大多數高階函式都是在其它各種不同的傳統型函數語言程式設計語言中發展出來的高階函式,其有用性已經過多年的實踐驗證。

可能其中最著名、最有用和最重要的高階函式要數curry()了。函式curry()的名字取自於邏輯學家Haskell Curry,前文提及的一種程式語言也是用他姓名當中的名字部分命名的。”currying”背後隱含的意思是,(幾乎)每一個函式都可以視為只帶一個引數的部分函式(partial function)。要使currying能夠用起來所需要做的就是讓函式本身的返回值也是個函式,只不過所返回的函式“縮小了範圍”或者是“更加接近完整的函式”。這和我在第二部分中提到的閉包特別相似 —— 對經過curry後的返回的後繼函式進行呼叫時一步一步“填入”最後計算所需的更多資料(附加到一個過程(procedure)之上的資料)
現在讓我們先用Haskell中一個很簡單例子對curry進行講解,然後在Python中使用functional模組重複展示一下這個簡單的例子:
在Haskell計算中使用Curry

現在使用Python:

在Python計算中使用Curry

第二部分中提到過的一個簡單的計稅程式的例子,當時用的是閉包(這次使用curry()),可以用來進一步做個對比:

Python中curry後的計稅程式

和使用閉包不同,我們需要以特定的順序(從左到右)對引數進行curry處理。當要注意的是,functional模組中還包含一個rcurry()類,能夠以相反的方向進行curry處理(從右到左)。
從一個層面講,其中的第二個print語句同簡單的同普通的taxcalc(50000,0.30,10000)函式呼叫相比只是個微小的拼寫方面的變化。但從另一個不同的層面講,它清晰地一個概念,那就是,每個函式都可以變換成僅僅帶有一個引數的函式,這對於剛剛接觸這個概念的人來講,會有一種特別驚奇的感覺。

其它高階函式

除了上述的curry功能,functional模組簡直就是一個很有意思的高階函式萬能口袋。此外,無論用還是不用functional模組,編寫你自己的高階函式真的並不難。至少functional模組中的那些高階函式為你提供了一些很值一看的思路。
它裡面的其它高階函式在很大程度上感覺有點象是“增強”版本的標準高階函式map()、filter()和reduce()。這些函式的工作模式通常大致如此:將一個或多個函式以及一些列表作為引數接收進來,然後對這些列表引數執行它前面所接收到的函式。在這種工作模式方面,有非常大量很有意思也很有用的擺弄方法。還有一種模式是:拿到一組函式後,將這組函式的功能組合起來建立一個新函式。這種模式同樣也有大量的變化形式。下面讓我們看看functional模組裡到底還有哪些其它的高階函式。

sequential()和also()這兩個函式都是在一系列成分函式(component function)的基礎上建立一個新函式。然後這些成分函式可以通過使用相同的引數進行呼叫。兩者的主要區別就在於,sequential()需要一個單個的函式列表作為引數,而also()接受的是一系列的多個引數。在多數情況下,對於函式的副作用而已這些會很有用,只是sequential()可以讓你隨意選擇將哪個函式的返回值作為組合起來後的新函式的返回值。
順序呼叫一系列函式(使用相同的引數)

isjoin()和conjoin()這兩個函式同equential()和also()在建立新函式並對引數進行多個成分函式的呼叫方面非常相似。只是disjoin()函式用來查詢成分函式中是否有一個函式的返回值(針對給定的引數)為真;conjoin()函式用來查詢是否所有的成分函式的返回值都為真。在這些函式中只要條件允許就會使用邏輯短路,因此disjoin()函式可能不會出現某些副作用。joinfuncs()i同also()類似,但它返回的是由所有成分函式的返回值組成的一個元組(tuple),而不是選中的某個主函式。

前文所述的幾個函式讓你可以使用相同的引數對一系列函式進行呼叫,而any()、all()和 none_of()這三個讓你可以使用一個引數列表對同一個函式進行多次呼叫。在大的結構方面,這些函式同內建的map()、reduce()和filter()有點象。 但funtional模組中的這三個高階函式中都是對一組返回值進行布林(boolean)運算得到其返回值的。例如:
對一系列返回值的真、假情況進行判斷

有點數學基礎的人會對這個高階函式非常感興趣:iscompose(). 將多個函式進行合成(compostion)指的是,將一個函式的返回值同下個函式的輸入“連結到一起”。對多個函式進行合成的程式設計師需要負責保證函式間的輸入和輸出是相互匹配的,不過這個條件無論是程式設計師在何時想使用返回值時都是需要滿足的。舉個簡單的例子和闡明這一點:
建立合成函式

後會有期

衷心希望我對高階函式的思考能夠引起讀者的興趣。無論如何,請動手試一試。試著編寫一些你自己的高階函式;一些可能很有用,很強大。告訴我它如何執行;或許這個系列之後的章節會討論讀者不斷提供的新觀點,新想法。

相關文章