很早以前就聽說過了函數語言程式設計,印象中是一種很晦澀難懂的程式設計模式,但卻一直沒有去進行了解。
恰好這周組內的週會輪到我主持,一時也沒想到要分享什麼。靈光一閃,就選定函數語言程式設計這個主題吧,反正組裡的同事都沒有學過,只需要講解入門方面的知識就好,也正好可以借這個機會逼迫自己去學習下這種新的程式設計方式。
經過初步瞭解,發現支援函數語言程式設計的語言挺多的,除了像Lisp、Scheme、Haskell、Erlang這樣專用的函數語言程式設計語言,我們常用的好多通用型程式語言(如Java、Python、Ruby、Javascript等)都支援函數語言程式設計模式。考慮了下實際情況,最終還是選擇Python作為函數語言程式設計的入門語言,因為組內同事都熟悉Python,以此作為切入點不會產生太大困難。
經過查詢資料和初步學習,對函數語言程式設計有了些概念,經過整理,便形成了分享PPT。
以下便是這次分享的內容。
目標
通常,我們在新學習一門技術或者程式語言的時候,通常都會先從相關概念和特性入手。對於新接觸函數語言程式設計的人來說,可能會想知道如下幾點:
- 什麼是函數語言程式設計?
- 函數語言程式設計的特點?
- 函數語言程式設計的用途?
- 函數語言程式設計相比於指令式程式設計和麵向物件程式設計的優缺點?
但是我這次分享卻沒有按照這個思路,因為我感覺在一開始就向聽眾灌輸太多概念性的東西,反倒會讓聽眾感到迷糊。因為經過查詢資料發現,對於什麼是函式化程式設計,很難能有一個協調一致的定義。而且由於我也是新接觸,自身的理解可能會存在較大的偏差。
因此,我決定分享內容儘量從大家熟悉的指令式程式設計切入,通過大量例項來向聽眾展現函數語言程式設計思維方式的不同之處。在這之後,再回過頭看這幾個問題,相信聽眾應該都會有更深刻的理解。
考慮到實際情況,本次分享希望能達成的目標是:
- 瞭解函數語言程式設計與指令式程式設計的主要區別
- 掌握Python語言函數語言程式設計的基本函式和運算元
- 會將簡單的指令式程式設計語句轉換為函數語言程式設計
指令式程式設計 & 函數語言程式設計
首先從大家熟悉的指令式程式設計開始,我們先回顧下平時在寫程式碼時主要的情景。
其實,不管我們的業務程式碼有多複雜,都離不開以下幾類操作:
- 函式定義:def
- 條件控制:if, elif, else
- 迴圈控制:for, break, continue, while
當然,這只是部分操作型別,除此之外還應該有類和模組、異常處理等等。但考慮到是入門,我們就先只關注上面這三種最常見的操作。
對應地,函數語言程式設計也有自己的關鍵字。在Python語言中,用於函數語言程式設計的主要由3個基本函式和1個運算元。
- 基本函式:map()、reduce()、filter()
- 運算元(operator):lambda
令人驚訝的是,僅僅採用這幾個函式和運算元就基本上可以實現任意Python程式。
當然,能實現是一回事兒,實際編碼時是否這麼寫又是另外一回事兒。估計要真只採用這幾個基本單元來寫所有程式碼的話,不管是在表達上還是在閱讀上應該都挺彆扭的。不過,嘗試採用這幾個基本單元來替代上述的函式定義、條件控制、迴圈控制等操作,對理解函數語言程式設計如何通過函式和遞迴表達流程控制應該會很有幫助。
在開始嘗試將指令式程式設計轉換為函數語言程式設計之前,我們還是需要先熟悉下這幾個基本單元。
Python函數語言程式設計的基本單元
lambda
lambda這個關鍵詞在很多語言中都存在。簡單地說,它可以實現函式建立的功能。
如下便是lambda的兩種使用方式。
1 2 3 |
func1 = lambda : <expression()> func2 = lambda x : <expression(x)> func3 = lambda x,y : <expression(x,y)> |
在第一條語句中,採用lambda建立了一個無參的函式func1。這和下面採用def建立函式的效果是相同的。
1 2 |
def func1(): <expression()> |
在第二條和第三條語句中,分別採用lambda建立了需要傳入1個引數的函式func2,以及傳入2個引數的函式func3。這和下面採用def
建立函式的效果是相同的。
1 2 3 4 5 |
def func2(x): <expression(x)> def func3(x,y): <expression(x,y)> |
需要注意的是,呼叫func1的時候,雖然不需要傳入引數,但是必須要帶有括號()
,否則返回的只是函式的定義,而非函式執行的結果。這個和在ruby中呼叫無參函式時有所不同,希望ruby程式設計師引起注意。
1 2 3 4 5 |
>>> func = lambda : 123 >>> func <function <lambda> at 0x100f4e1b8> >>> func() 123 |
另外,雖然在上面例子中都將lambda建立的函式賦值給了一個函式名,但這並不是必須的。從下面的例子中大家可以看到,很多時候我們都是直接呼叫lambda建立的函式,而並沒有命名一個函式,這也是我們常聽說的匿名函式的由來。
map()
map()
函式的常見呼叫形式如下所示:
1 |
map(func, iterable) |
map()
需要兩個必填引數,第一個引數是一個函式名,第二個引數是一個可迭代的物件,如列表、元組等。
map()
實現的功能很簡單,就是將第二個引數(iterable)中的每一個元素分別傳給第一個引數(func),依次執行函式得到結果,並將結果組成一個新的list
物件後進行返回。返回結果永遠都是一個list
。
簡單示例如下:
1 2 3 |
>>> double_func = lambda s : s * 2 >>> map(double_func, [1,2,3,4,5]) [2, 4, 6, 8, 10] |
除了傳入一個可迭代物件這種常見的模式外,map()
還支援傳入多個可迭代物件。
1 |
map(func, iterable1, iterable2) |
在傳入多個可迭代物件的情況下,map()
會依次從所有可迭代物件中依次取一個元素,組成一個元組列表,然後將元組依次傳給func;若可迭代物件的長度不一致,則會以None進行補上。
通過以下示例應該就比較容易理解。
1 2 3 4 5 6 7 |
>>> plus = lambda x,y : (x or 0) + (y or 0) >>> map(plus, [1,2,3], [4,5,6]) [5, 7, 9] >>> map(plus, [1,2,3,4], [4,5,6]) [5, 7, 9, 4] >>> map(plus, [1,2,3], [4,5,6,7]) [5, 7, 9, 7] |
在上面的例子中,之所以採用x or 0
的形式,是為了防止None + int
出現異常。
需要注意的是,可迭代物件的個數應該與func的引數個數一致,否則就會出現異常,因為傳參個數與函式引數個數不一致了,這個應該比較好理解。
1 2 3 4 5 |
>>> plus = lambda x,y : x + y >>> map(plus, [1,2,3]) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: <lambda>() takes exactly 2 arguments (1 given) |
另外,map()
還存在一種特殊情況,就是func為None。這個時候,map()
仍然是從所有可迭代物件中依次取一個元素,組成一個元組列表,然後將這個元組列表作為結果進行返回。
1 2 3 4 5 6 7 8 |
>>> map(None, [1,2,3,4]) [1, 2, 3, 4] >>> map(None, [1,2,3,4], [5,6,7,8]) [(1, 5), (2, 6), (3, 7), (4, 8)] >>> map(None, [1,2,3,4], [5,6,7]) [(1, 5), (2, 6), (3, 7), (4, None)] >>> map(None, [1,2,3,4], [6,7,8,9], [11,12]) [(1, 6, 11), (2, 7, 12), (3, 8, None), (4, 9, None)] |
reduce()
reduce()
函式的呼叫形式如下所示:
1 |
reduce(func, iterable[, initializer]) |
reduce()
函式的功能是對可迭代物件(iterable)中的元素從左到右進行累計運算,最終得到一個數值。第三個引數initializer是初始數值,可以空置,空置為None時就從可迭代物件(iterable)的第二個元素開始,並將第一個元素作為之前的結果。
文字描述可能不大清楚,看下reduce()
的原始碼應該就比較清晰了。
1 2 3 4 5 6 7 8 9 10 11 |
def reduce(function, iterable, initializer=None): it = iter(iterable) if initializer is None: try: initializer = next(it) except StopIteration: raise TypeError('reduce() of empty sequence with no initial value') accum_value = initializer for x in it: accum_value = function(accum_value, x) return accum_value |
再加上如下示例,對reduce()
的功能應該就能掌握了。
1 2 3 4 5 |
>>> plus = lambda x, y : x + y >>> reduce(plus, [1,2,3,4,5]) 15 >>> reduce(plus, [1,2,3,4,5], 10) 25 |
filter()
filter()
函式的呼叫形式如下:
1 |
filter(func, iterable) |
filter()
有且僅有兩個引數,第一個引數是一個函式名,第二個引數是一個可迭代的物件,如列表、元組等。
filter()
函式的呼叫形式與map()
比較相近,都是將第二個引數(iterable)中的每一個元素分別傳給第一個引數(func),依次執行函式得到結果;差異在於,filter()
會判斷每次執行結果的bool
值,並只將bool
值為true
的篩選出來,組成一個新的列表並進行返回。
1 2 3 |
>>> mode2 = lambda x : x % 2 >>> filter(mode2, [1,2,3,4,5,6,7,8,9,10]) [1, 3, 5, 7, 9] |
以上便是Python函數語言程式設計基本單元的核心內容。
接下來,我們就開始嘗試採用新學習到的基本單元對指令式程式設計中的條件控制
和迴圈控制
進行轉換。
替換條件控制語句
在對條件控制
進行替換之前,我們先來回顧下Python中對布林表示式求值時進行的“短路”處理。
什麼叫“短路”處理?簡單地講,就是如下兩點:
- 在
f(x) and g(y)
中,當f(x)
為false
時,不會再執行g(y)
,直接返回false
- 在
f(x) or g(y)
中,當f(x)
為true
時,不會再執行g(y)
,直接返回true
結論是顯然易現的,就不再過多解釋。
那麼,對應到條件控制語句,我們不難理解,如下條件控制語句和表示式是等價的。
1 2 3 4 |
# flow control statement if <cond1>: func1() elif <cond2>: func2() else: func3() |
1 2 |
# Equivalent "short circuit" expression (<cond1> and func1()) or (<cond2> and func2()) or (func3()) |
通過這個等價替換,我們就去除掉了if/elif/else
關鍵詞,將條件控制語句轉換為一個表示式。那這個表示式和函數語言程式設計有什麼關係呢?
這時我們回顧上面講過的lambda
,會發現lambda
運算元返回的就是一個表示式。
基於這一點,我們就可以採用lambda
建立如下函式。
1 2 3 4 5 6 7 8 9 10 |
>>> pr = lambda s:s >>> print_num = lambda x: (x==1 and pr("one")) .... or (x==2 and pr("two")) .... or (pr("other")) >>> print_num(1) 'one' >>> print_num(2) 'two' >>> print_num(3) 'other' |
通過函式呼叫的結果可以看到,以上函式實現的功能與之前的條件控制語句實現的功能完全相同。
到這裡,我們就實現了命令式條件控制語句向函式式語句的轉換。並且這個轉換的方法是通用的,所有條件控制語句都可以採用這種方式轉換為函式式語句。
替換迴圈控制語句
接下來我們再看迴圈控制
語句的轉換。在Python中,迴圈控制是通過for
和while
這兩種方式實現的。
替換for迴圈
for
迴圈語句的替換十分簡單,採用map()
函式就能輕鬆實現。這主要是因為for
語句和map()
原理相同,都是對可迭代物件裡面的每一個元素進行操作,因此轉換過程比較自然。
1 2 3 4 5 |
# statement-based for loop for e in lst: func(e) # Equivalent map()-based loop map(func, lst) |
1 2 3 4 5 6 7 8 9 10 |
>>> square = lambda x : x * x >>> for x in [1,2,3,4,5]: square(x) ... 1 4 9 16 25 >>> map(square, [1,2,3,4,5]) [1, 4, 9, 16, 25] |
替換while迴圈
while
迴圈語句的替換相比而言就複雜了許多。
下面分別是while
迴圈語句及其對應的函式式風格的程式碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# statement-based while loop while <condition>: <pre-suite> if <break_condition>: break else: <suite> # Equivalent FP-style recursive while loop def while_block(): <pre-suite> if <break_condition>: return 1 else: <suite> return 0 while_FP = lambda: <condition> and (while_block() or while_FP()) while_FP() |
這裡的難點在於,函式式while_FP
迴圈採用了遞迴的概念。當為
true
時,進入迴圈體,執行while_block()
;若為
true
時,返回1,while_FP()
呼叫結束;若為
false
時,返回0,會繼續執行or
右側的while_FP()
,從而實現遞迴呼叫;若始終為
false
,則會持續遞迴呼叫while_FP()
,這就實現了while
語句中同樣的功能。
為了對函式式的while
迴圈有更深刻的理解,可以再看下如下示例。這個例子是在網上找的,實現的是echo
功能:輸入任意非”quit”字元時,列印輸入的字元;輸入”quit”字元時,退出程式。
1 2 3 4 5 6 7 8 9 10 11 12 |
➜ PythonFP python pyecho.py IMP -- 1 1 IMP -- 2 2 IMP -- abc abc IMP -- 1 + 1 1 + 1 IMP -- quit quit ➜ PythonFP |
如下便是分別採用過程式和函式式語句實現的”echo”功能。
1 2 3 4 5 6 7 8 9 |
# imperative version of "echo()" def echo_IMP(): while 1: x = raw_input("IMP -- ") print x if x == 'quit': break echo_IMP() |
1 2 3 4 5 6 7 |
def monadic_print(x): print x return x # FP version of "echo()" echo_FP = lambda: monadic_print(raw_input("FP -- "))=='quit' or echo_FP() echo_FP() |
更多示例
到此為止,我們對函數語言程式設計總算有了點認識,到達之前設定的目標應該是沒有問題了,看來函數語言程式設計也並沒有想象中的那麼難懂。
然而,這都只是函數語言程式設計的皮毛而已,不信?再看下如下示例。
這個示例也是在網上找的,實現的是兩個列表笛卡爾積的篩選功能,找出笛卡爾積元組集合中兩個元素之積大於25的所有元組。
1 2 3 4 5 6 7 |
bigmuls = lambda xs,ys: filter(lambda (x,y):x*y > 25, combine(xs,ys)) combine = lambda xs,ys: map(None, xs*len(ys), dupelms(ys,len(xs))) dupelms = lambda lst,n: reduce(lambda s,t:s+t, map(lambda l,n=n: [l]*n, lst)) print bigmuls([1,2,3,4],[10,15,3,22]) [(3, 10), (4, 10), (2, 15), (3, 15), (4, 15), (2, 22), (3, 22), (4, 22)] |
雖然這個例子中lambda/map/reduce/filter
都是我們已經比較熟悉了的基本單元,但是經過組合後,理解起來還是會比較吃力。
總結
看到這裡,有的同學就開玩笑說我這標題名稱非常貼切,《Python的函數語言程式設計–從入門到⎡放棄⎦》,因為以後在工作中應該也不會再嘗試使用函數語言程式設計了,^_^。
不過,我還是覺得函數語言程式設計挺有意思的,更高階的特性後面值得再繼續學習。即使程式碼不用寫成pure函式式風格,但在某些時候區域性使用lambda/map/reduce/filter
也能大大簡化程式碼,也是一個不錯的選擇。
另外,通過此次分享,再次切身體會到了教授是最好的學習方式,只有當你真正能將一個概念講解清楚的時候,你才算是掌握了這個概念。
參考連結
http://www.ibm.com/developerworks/linux/library/l-prog/index.html