新聞中心
摘要:雖然人們總把Python當(dāng)作過(guò)程化的,面向?qū)ο蟮恼Z(yǔ)言,但是他實(shí)際上包含了函數(shù)化編程中,你需要的任何東西。這篇文章主要討論函數(shù)化編程的一般概念,并說(shuō)明用Python來(lái)函數(shù)化編程的技術(shù)。

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


咨詢(xún)
建站咨詢(xún)
