可愛的Python函數(shù)式編程(一)
摘要:雖然人們總把Python當(dāng)作過程化的,面向?qū)ο蟮恼Z言,但是他實際上包含了函數(shù)化編程中,你需要的任何東西。這篇文章主要討論函數(shù)化編程的一般概念,并說明用Python來函數(shù)化編程的技術(shù)。
我們最好從艱難的問題開始出發(fā):“到底什么是函數(shù)化編程呢?”其中一個答案可能是這樣的,函數(shù)化編程就是你在使用Lisp這樣的語言時所做的(還有Scheme,Haskell,ML,OCAML,Mercury,Erlang和其他一些語言)。這是一個保險的回答,但是它解釋得并不清晰。不幸的是對于什么是函數(shù)化編程,很難能有一個協(xié)調(diào)一致的定義,即使是從函數(shù)化變成本身出發(fā),也很難說明。這點倒很像盲人摸象。不過,把它拿來和命令式編程(imperative programming)做比較也不錯(命令式編程就像你在用C,Pascal,C++,Java,Perl,Awk,TCL和很多其他類似語言時所做的,至少大部分一樣 )。
我個人粗略總結(jié)了一下,認(rèn)為函數(shù)式編程至少應(yīng)該具有下列幾點中的多個特點。在謂之為函數(shù)式的語言中,要做到這些就比較容易,但要做到其它一些事情不是很難就是完全不可能:
·函數(shù)具有首要地位 (對象)。也就是說,能對“數(shù)據(jù)”做什么事,就要能對函數(shù)本身做到那些事(比如將函數(shù)作為參數(shù)傳遞給另外一個函數(shù))。
·將遞歸作為主要的控制結(jié)構(gòu)。在有些函數(shù)式語言中,都不存在其它的“循環(huán)”結(jié)構(gòu)。
·列表處理作為一個重點(例如,Lisp語言的名字)。列表往往是通過對子列表進(jìn)行遞歸取代了循環(huán)。
·“純”函數(shù)式語言會完全避免副作用。這么做就完全棄絕了命令式語言中幾乎無處不在的這種做法:將第一個值賦給一個變量之后為了跟蹤程序的運(yùn)行狀態(tài),接著又將另外一個值賦給同一個變量。
·函數(shù)式編程不是不鼓勵就是完全禁止使用語句,而是通過對表達(dá)式(換句話說,就是函數(shù)加上參數(shù))求值(evaluation of expressions)完成任務(wù). 在最純粹的情形下,一個程序就是一個表達(dá)式(再加上輔助性的定義)
·函數(shù)式編程中最關(guān)心的是要對什么進(jìn)行計算,而不是要怎么來進(jìn)行計算。
·在很多函數(shù)式編程語言中都會用到“高階”(higher order)函數(shù) (換句話說,高階函數(shù)就是對對函數(shù)進(jìn)行運(yùn)算的函數(shù)進(jìn)行運(yùn)算的函數(shù))。
函數(shù)式編程的倡導(dǎo)者們認(rèn)為,所有這些特性都有助于更快地編寫出更多更簡潔并且更不容易出Bug的代碼。而且,計算機(jī)科學(xué)、邏輯學(xué)和數(shù)學(xué)這三個領(lǐng)域中的高級理論家發(fā)現(xiàn),函數(shù)式編程語言和程序的形式化特性在證明起來比命令式編程語言和程序要簡單很多。
Python內(nèi)在的函數(shù)式功能
自Python 1.0起,Python就已具有了以上所列中的絕大多數(shù)特點。但是就象Python所具有的大多數(shù)特性一樣,這些特點出現(xiàn)在了一種混合了各種特性的語言中。 和Python的OOP(面向?qū)ο缶幊蹋?特性非常象,你想用多少就用多少,剩下的都可以不管(直到你隨后需要用到它們?yōu)橹梗T赑ython 2.0中,加入了列表解析(list comprehensions)這個非常好用的”語法糖“。 盡管列表解析沒有添加什么新功能,但它讓很多舊功能看起來好了不少。
Python中函數(shù)式編程的基本要素包括functionsmap()、reduce()、filter()和lambda算子(operator)。 在Python 1.x中,apply()函數(shù)也可以非常方便地拿來將一個函數(shù)的列表返回值直接用于另外一個函數(shù)。Python 2.0為此提供了一個改進(jìn)后的語法??赡苡悬c讓人驚奇,使用如此之少的函數(shù)(以及基本的算子)幾乎就足以寫出任何Python程序了;更加特別的是,幾乎用不著什么執(zhí)行流程控制語句。
所有(if,elif,else,assert,try,except,finally,for,break,continue,while,def)這些都都能通過僅僅使用函數(shù)式編程中的函數(shù)和算子就能以函數(shù)式編程的風(fēng)格處理好。盡管真正地在程序中完全排除使用所有流程控制命令可能只在想?yún)⒓?rdquo;Python混亂編程“大賽(可將Python代碼寫得跟Lisp代碼非常象)時才有意義,但這對理解函數(shù)式編程如何通過函數(shù)和遞歸表達(dá)流程控制很有價值。
剔除流程控制語句
剔除練習(xí)首先要考慮的第一件事是,實際上,Python會對布爾表達(dá)式求值進(jìn)行“短路”處理。這就為我們提供了一個if/elif/else分支語句的表達(dá)式版(假設(shè)每個分支只調(diào)用一個函數(shù),不是這種情況時也很容易組織成重新安排成這種情況)。 這里給出怎么做:
對Python中的條件調(diào)用進(jìn)行短路處理
- # Normal statement-based flow control
- if <cond1>: func1()
- elif <cond2>: func2()
- else: func3()
- # Equivalent "short circuit" expression
- (<cond1> and func1()) or (<cond2> and func2()) or (func3())
- # Example "short circuit" expression
- >>> x = 3
- >>> def pr(s): return s
- >>> (x==1 and pr('one')) or (x==2 and pr('two')) or (pr('other'))
- 'other'
- >>> x = 2
- >>> (x==1 and pr('one')) or (x==2 and pr('two')) or (pr('other'))
- 'two'
我們的表達(dá)式版本的條件調(diào)用看上去可能不算什么,更象是個小把戲;然而,如果我們注意到lambda算子必須返回一個表達(dá)式,這就更值得關(guān)注了。既然如我們所示,表達(dá)式能夠通過短路包含一個條件判斷,那么,lambda表達(dá)式就是個完全通用的表達(dá)條件判斷返回值的手段了。我們來一個例子:
Python中短路的Lambda
- >>> pr = lambda s:s
- >>> namenum = lambda x: (x==1 and pr("one")) \
- .... or (x==2 and pr("two")) \
- .... or (pr("other"))
- >>> namenum(1)
- 'one'
- >>> namenum(2)
- 'two'
- >>> namenum(3)
- 'other'
將函數(shù)作為具有首要地位的對象
前面的例子已經(jīng)表明了Python中函數(shù)具有首要地位,但有點委婉。當(dāng)我們用lambda操作創(chuàng)建一個函數(shù)對象時, 我們所得到的東西是完全通用的。就其本質(zhì)而言,我們可以將我們的對象同名字"pr"和"namenum"綁定到一起, 以完全相同的方式,我們也也完全可以將數(shù)字23或者字符串"spam" 同這些名字綁定到一起。但是,就象我們可以無需將其綁定到任何名字之上就能直接使用數(shù)字23(也就是說,它可以用作函數(shù)的參數(shù))一樣,我們也可以直接使用我們使用lambda創(chuàng)建的函數(shù)對象,而無需將其綁定到任何名字之上。在Python中,函數(shù)就是另外一種我們能夠就像某種處理的值。
我們對具有首要地位的對象做的比較多的事情就是,將它們作為參數(shù)傳遞給函數(shù)式編程固有的函數(shù)map()、reduce()和filter()。這三個函數(shù)接受的第一個參數(shù)都是一個函數(shù)對象。
·map()針對指定給它的一個或多個列表中每一項對應(yīng)的內(nèi)容,執(zhí)行一次作為參數(shù)傳遞給它的那個函數(shù) ,最后返回一個結(jié)果列表。
·reduce()針對每個后繼項以及最后結(jié)果的累積結(jié)果,執(zhí)行一次作為參數(shù)傳遞給它的那個函數(shù);例如,reduce(lambda n,m:n*m, range(1,10))是求"10的階乘"的意思(換言之,將每一項和前面所得的乘積進(jìn)行相乘)
·filter()使用那個作為參數(shù)傳遞給它的函數(shù),對一個列表中的所有項進(jìn)行”求值“,返回一個由所有能夠通過那個函數(shù)測試的項組成的經(jīng)過遴選后的列表。
我們經(jīng)常也會把函數(shù)對象傳遞給我們自己定義的函數(shù),不過一般情況下這些自定義的函數(shù)就是前文提及的內(nèi)建函數(shù)的某種形式的組合。
通過組合使用這三種函數(shù)式編程內(nèi)建的函數(shù), 能夠?qū)崿F(xiàn)范圍驚人的“執(zhí)行流程”操作(全都不用語句,僅僅使用表達(dá)式實現(xiàn))。
Python中的函數(shù)式循環(huán)
替換循環(huán)語言和條件狀態(tài)語言塊同樣簡單。for可以直接翻譯成map()函數(shù)。正如我們的條件執(zhí)行,我們會需要簡化語句塊成簡單的函數(shù)調(diào)用(我們正在接近通常能做的):
替換循環(huán)
- for e in lst: func(e) # statement-based loop
- map(func,lst) # map()-based loop
通過這種方法,對有序程序流將有一個相似的函數(shù)式方式。那就是,命令式編程幾乎是由大量“做這,然后做那,之后做其它的”語句組成。map()讓我們只要做這樣:
Map-based 動作序列
- # let's create an execution utility function
- do_it = lambda f: f()
- # let f1, f2, f3 (etc) be functions that perform actions
- map(do_it, [f1,f2,f3]) # map()-based action sequence
通常,我們的整個主要的程序都可以使用一個map表達(dá)式加上一些函數(shù)列表的執(zhí)行來完成這個程序。最高級別的函數(shù)的另一個方便的特性是你可以把它們放在一個列表里。
翻譯while會稍稍復(fù)雜一些,但仍然可以直接地完成:
Python中的函數(shù)式"while"循環(huán)
- # statement-based while loop
- while <cond>:
- <pre-suite>
- if <break_condition>:
- break
- else:
- <suite>
- # FP-style recursive while loop
- def while_block():
- <pre-suite>
- if <break_condition>:
- return 1
- else:
- <suite>
- return 0
- while_FP = lambda: (<cond> and while_block()) or while_FP()
- while_FP()
在翻譯while循環(huán)時,我們?nèi)匀恍枰褂脀hile_block()函數(shù),這個函數(shù)本身里面可以包含語句而不是僅僅包含表達(dá)式。但我們可能還能夠?qū)@個函數(shù)再進(jìn)行更進(jìn)一步的剔除過程(就像前面模版中的對if/else進(jìn)行短路處理一樣)。 還有,<cond>很難對普通的測試有什么用,比如while myvar==7,既然循環(huán)體(在設(shè)計上)不能對任何變量的值進(jìn)行修改(當(dāng)然,在while_block()中可以修改全局變量)。有一種方法可以用來為 while_block()添加更有用的條件判斷,讓 while_block()返回一個有意義的值,然后將這個返回值同循環(huán)結(jié)束條件進(jìn)行比較?,F(xiàn)在應(yīng)該來看一個剔除其中語句的具體例子了:
Python中'echo'循環(huán)
- # imperative version of "echo()"
- def echo_IMP():
- while 1:
- x = raw_input("IMP -- ")
- if x == 'quit':
- break
- else
- print x
- echo_IMP()
- # utility function for "identity with side-effect"
- 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()
在上面的例子中我們所做的,就是想辦法將一個涉及I/O、循環(huán)和條件判斷的小程序,表達(dá)為一個遞歸方式的純粹的表達(dá)式 (確切地說,表達(dá)為一個可以在需要的情況下傳遞到別的地方的函數(shù)對象)。我們 的確仍然使用了實用函數(shù)monadic_print(),但這個函數(shù)是完全通用的,而且可以用于以后我們可能會創(chuàng)建的每個函數(shù)式程序的表達(dá)式中(它的代價是一次性的)。請注意,任何包含monadic_print(x)的表達(dá)式的 值都是一樣的,好像它只是包含了一個x而已。函數(shù)式編程中(特別是在Haskell中)的函數(shù)有一種叫做"monad"(一元)的概念,這種一元函數(shù)“實際什么都不做,只是在執(zhí)行過程中產(chǎn)生一個副作用”。
避免副作用
在做完這些沒有非常明智的理由陳述,并把晦澀的嵌套表達(dá)式代替他們之后,一個很自然的問題是“為什么要這樣做?!” 我描述的函數(shù)式編程在Python中都實現(xiàn)了。但是最重要的特性和一個有具體用處——就是避免副作用(或至少它們阻止如monads的特殊區(qū)域)。程序錯誤的大部分——并且這些問題驅(qū)使程序員去debug——出現(xiàn)是因為在程序的運(yùn)行中變量獲取了非期望的值。函數(shù)式編程簡單地通過從不給變量賦值而繞過了這個問題。
現(xiàn)在讓我們看一段非常普通的命令式代碼。這段代碼的目的是打印出乘積大于25的一對一對數(shù)字所組成的一個列表。組成每對數(shù)字的每一個數(shù)字都是取自另外的兩個列表。這種事情和很多程序員在他們的編程中經(jīng)常做的一些事情比較相似。命令式的解決方式有可能就象下面這樣:
命令式的"打印大乘積"的Python代碼
- # Nested loop procedural style for finding big products
- xs = (1,2,3,4)
- ys = (10,15,3,22)
- bigmuls = []
- # ...more stuff...
- for x in xs:
- for y in ys:
- # ...more stuff...
- if x*y > 25:
- bigmuls.append((x,y))
- # ...more stuff...
- # ...more stuff...
- print bigmuls
這個項目足夠小了,好像沒有地方會出什么差錯。但有可能在這段代碼中我們會嵌入一些同時完成其它任務(wù)的代碼。用"more stuff"(其它代碼)注釋掉的部分,就是有可能存在導(dǎo)致出現(xiàn)bug的副作用的地方。在那三部分的任何一點上,變量sxs、ys、bigmuls、x、y都有可能在這段按照理想情況簡化后的代碼中取得一個出人意料的值。還有,這段代碼執(zhí)行完后,后繼代碼有可能需要也有可能不需要對所有這些變量中的值有所預(yù)期。顯而易見,將這段代碼封裝到函數(shù)/實例中,小心處理變量的作用范圍,就能夠避免這種類型的錯誤。你也可以總是將使用完畢的變量del掉。但在實踐中,這里指出的這種類型的錯誤很常見。
以一種函數(shù)式的途徑一舉消除這些副作用所產(chǎn)生的錯誤,這樣就達(dá)到了我們的目的。一種可能的代碼如下:
以函數(shù)式途徑達(dá)到我們的目的
- 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))
在例子中我們綁定我們的匿名(lambda)函數(shù)對象到變量名,但嚴(yán)格意義上講這并不是必須的。我們可以用簡單的嵌套定義來代替之。這不僅是為了代碼的可讀性,我們才這樣做的;而且是因為combine()函數(shù)在任何地方都是一個非常好的功能函數(shù)(函數(shù)從兩個輸入的列表讀入數(shù)據(jù)生成一個相應(yīng)的pair列表)。函數(shù)dupelms()只是用來輔助函數(shù)combine()的。即使這個函數(shù)式的例子跟命令式的例子顯得要累贅些,不過一旦你考慮到功能函數(shù)的重用,則新的bigmuls()中代碼就會比命令式的那個要稍少些。
這個函數(shù)式例子的真正優(yōu)點在于:在函數(shù)中絕對沒有改變變量的值。這樣就不可能在之后的代碼(或者從之前的代碼)中產(chǎn)生不可預(yù)期的副作用。顯然,在函數(shù)中沒有副作用,并不能保證代碼的正確性,但它仍然是一個優(yōu)點。無論如何請注意,Python(不像很多其它的函數(shù)式語言)不會阻止名字bigmuls,combine和dupelms的再次綁定。如果combine()運(yùn)行在之后的程序中意味著有所不同時,所有的預(yù)測都會失效。你可能會需要新建一個單例類來包含這個不變的綁定(也就是說,s.bigmuls之類的);但是這一例并沒有空間來做這些。
一個明顯值得注意的是,我們特定的目標(biāo)是定制Python 2的一些特性。而不是命令式的或函數(shù)式編程的例子,最好的(也是函數(shù)式的)方法是:
- print [(x,y) for x in (1,2,3,4) for y in (10,15,3,22) if x*y > 25]
結(jié)束語
我已經(jīng)列出了把每一個Python控制流替換成一個相等的函數(shù)式代碼的方法(在程序中減少副作用)。高效翻譯一個特定的程序需要一些額外的思考,但我們已經(jīng)看出內(nèi)置的函數(shù)式功能是全面且完善的。在接下來的文章里,我們會看到更多函數(shù)式編程的高級技巧;并且希望我們接下來能夠摸索到函數(shù)式編程風(fēng)格的更多優(yōu)點和缺點。
原文鏈接:http://www.oschina.net/translate/python-functional-programming-part1