文:鄭元春
人生苦短,我用Python几颜!
0x00:函數(shù)式編程
describe what to do, rather than how to do it.
函數(shù)式編程(英語(yǔ):functional programming)或稱函數(shù)程序設(shè)計(jì)脸候,又稱泛函編程穷娱,是一種編程典範(fàn),它將電腦運(yùn)算視為數(shù)學(xué)上的函數(shù)計(jì)算运沦,並且避免使用程序狀態(tài)以及易變物件泵额。函數(shù)程式語(yǔ)言最重要的基礎(chǔ)是λ演算(lambda calculus)。而且λ演算的函數(shù)可以接受函數(shù)當(dāng)作輸入(引數(shù))和輸出(傳出值)携添。
比起指令式編程嫁盲,函數(shù)式編程更加強(qiáng)調(diào)程序執(zhí)行的結(jié)果而非執(zhí)行的過(guò)程,倡導(dǎo)利用若干簡(jiǎn)單的執(zhí)行單元讓計(jì)算結(jié)果不斷漸進(jìn)烈掠,逐層推導(dǎo)復(fù)雜的運(yùn)算亡资,而不是設(shè)計(jì)一個(gè)復(fù)雜的執(zhí)行過(guò)程。
函數(shù)式編程就是一種抽象程度很高的編程范式向叉,純粹的函數(shù)式編程語(yǔ)言編寫的函數(shù)沒(méi)有變量锥腻,因此,任意一個(gè)函數(shù)母谎,只要輸入是確定的瘦黑,輸出就是確定的,這種純函數(shù)我們稱之為沒(méi)有副作用(邊界效應(yīng))奇唤。而允許使用變量的程序設(shè)計(jì)語(yǔ)言幸斥,由于函數(shù)內(nèi)部的變量狀態(tài)不確定,同樣的輸入咬扇,可能得到不同的輸出甲葬,因此,這種函數(shù)是有副作用的懈贺。
三大特點(diǎn)
1.immutable data(不可變數(shù)據(jù)):
默認(rèn)上變量是不可變的经窖,如果你要改變變量坡垫,你需要把變量copy出去修改
2.first class functions
這個(gè)技術(shù)可以讓你的函數(shù)就像變量一樣來(lái)使用。也就是說(shuō)画侣,你的函數(shù)可以像變量一樣被創(chuàng)建冰悠,修改,并當(dāng)成變量一樣傳遞配乱,返回或是在函數(shù)中嵌套函數(shù)溉卓。這個(gè)有點(diǎn)像Javascript的Prototype
3.尾遞歸優(yōu)化
我們知道遞歸的害處,那就是如果遞歸很深的話搬泥,stack受不了桑寨,并會(huì)導(dǎo)致性能大幅度下降。所以忿檩,我們使用尾遞歸優(yōu)化技術(shù)——每次遞歸時(shí)都會(huì)重用stack尉尾,這樣一來(lái)能夠提升性能,當(dāng)然休溶,這需要語(yǔ)言或編譯器的支持。Python就不支持扰她。
函數(shù)式編程的幾個(gè)技術(shù)
1.map & reduce
基于命令式的編程語(yǔ)言中兽掰,如果對(duì)一個(gè)數(shù)據(jù)集進(jìn)行特定操作的時(shí)候,需要使用for或者是while循環(huán)徒役,讓用戶在循環(huán)語(yǔ)句內(nèi)部進(jìn)行操作孽尽,并且所有的邊界條件都是用戶自己定義的,所以就會(huì)出現(xiàn)邊界溢出的bug忧勿。而map和reduce思想就是用一種更加數(shù)學(xué)化(理論化)的編程杉女。
2.Pipeline
之前map、reduce是將一組數(shù)據(jù)放到特定的函數(shù)里面進(jìn)行操作鸳吸,這里的操作函數(shù)一般就一個(gè)熏挎。管道(Pipeline)就像是Linux系統(tǒng)中的管道一樣。數(shù)據(jù)依此通過(guò)不同的管道晌砾,最后輸出坎拐。
3.recuring
遞歸最大的好處就簡(jiǎn)化代碼,他可以把一個(gè)復(fù)雜的問(wèn)題用很簡(jiǎn)單的代碼描述出來(lái)养匈。注意:遞歸的精髓是描述問(wèn)題哼勇,而這正是函數(shù)式編程的精髓。
4.currying
把一個(gè)函數(shù)的多個(gè)參數(shù)分解成多個(gè)函數(shù)呕乎, 然后把函數(shù)多層封裝起來(lái)积担,每層函數(shù)都返回一個(gè)函數(shù)去接收下一個(gè)參數(shù)這樣,可以簡(jiǎn)化函數(shù)的多個(gè)參數(shù)猬仁。而且每一層只專注最少的功能性代碼帝璧,所以你的代碼可維護(hù)性將大大提高先誉,出現(xiàn)bug的概率也會(huì)大大降低媳荒。
5.higher order function
高階函數(shù):所謂高階函數(shù)就是函數(shù)當(dāng)參數(shù)休玩,把傳入的函數(shù)做一個(gè)封裝底循,然后返回這個(gè)封裝函數(shù)≌鹜埃現(xiàn)象上就是函數(shù)傳進(jìn)傳出酵使,就像面向?qū)ο髮?duì)象滿天飛一樣驾凶。
6.lazy evaluation
惰性求值:這個(gè)需要編譯器的支持浊洞。表達(dá)式不在它被綁定到變量之后就立即求值藻丢,而是在該值被取用的時(shí)候求值把曼,也就是說(shuō)杨帽,語(yǔ)句如x:=expression; (把一個(gè)表達(dá)式的結(jié)果賦值給一個(gè)變量)明顯的調(diào)用這個(gè)表達(dá)式被計(jì)算并把結(jié)果放置到 x 中,但是先不管實(shí)際在 x 中的是什么嗤军,直到通過(guò)后面的表達(dá)式中到 x 的引用而有了對(duì)它的值的需求的時(shí)候注盈,而后面表達(dá)式自身的求值也可以被延遲,最終為了生成讓外界看到的某個(gè)符號(hào)而計(jì)算這個(gè)快速增長(zhǎng)的依賴樹叙赚。
7.__determinism __
確定性:所謂確定性的意思就是像數(shù)學(xué)那樣 f(x) = y 老客,這個(gè)函數(shù)無(wú)論在什么場(chǎng)景下,都會(huì)得到同樣的結(jié)果震叮,這個(gè)我們稱之為函數(shù)的確定性胧砰。而不是像程序中的很多函數(shù)那樣,同一個(gè)參數(shù)苇瓣,卻會(huì)在不同的場(chǎng)景下計(jì)算出不同的結(jié)果尉间。所謂不同的場(chǎng)景的意思就是我們的函數(shù)會(huì)根據(jù)一些運(yùn)行中的狀態(tài)信息的不同而發(fā)生變化。
總結(jié):函數(shù)式編程更側(cè)重的是讓你著手于解決問(wèn)題击罪,而不是解決問(wèn)題的過(guò)程或是方法哲嘲,一般從命令式語(yǔ)言(大多數(shù)的是C++)入門的程序員都會(huì)養(yǎng)成thinking like a machine 的思維方式,命令式語(yǔ)言已經(jīng)烙印在腦海中媳禁,就像那個(gè)冷笑話:
妻子對(duì)程序員老公說(shuō):去買點(diǎn)早飯眠副,如果有西瓜,買一個(gè)竣稽。結(jié)果程序員連早餐也沒(méi)有買侦啸。
老本行是數(shù)學(xué)的程序員他們對(duì)函數(shù)式編程的理解會(huì)比較深入一些,所以大家程序員們找媳婦的話還是造個(gè)數(shù)學(xué)的姑娘吧丧枪!
0x01:函數(shù)即數(shù)據(jù)
everything is data in computer, even all functions and your mind.
對(duì)于計(jì)算機(jī)來(lái)說(shuō)光涂,尤其是CPU,送到其中的任何東西都是數(shù)據(jù)(0和1嘛)拧烦。函數(shù)或者是類的概念只是更高層次的語(yǔ)言模型忘闻,對(duì)于底層的硬件來(lái)說(shuō)這些高層的語(yǔ)法和語(yǔ)義最終是要翻譯成指令進(jìn)入流水線的。所以說(shuō)函數(shù)也是數(shù)據(jù)的一種恋博,這里的數(shù)據(jù)并不是指狹義的數(shù)據(jù)齐佳,或者是在內(nèi)存塊中的地址私恬,而是廣義上的信息流。
在Python中我們知道炼吴,我們可以將一個(gè)函數(shù)賦值給一個(gè)變量本鸣。變量可以當(dāng)做參數(shù)傳遞到函數(shù)中,同時(shí)硅蹦,變量也可以作為函數(shù)的返回值返回荣德。那么這樣一來(lái),函數(shù)也能夠當(dāng)做參數(shù)傳遞到函數(shù)童芹,或是作為返回值(裝飾器一章節(jié)會(huì)講到這個(gè)函數(shù)作為返回值)返回涮瞻。
例子:
#函數(shù)作為參數(shù)
def callFunc(func):
print ("callFunc.")
func()
def Func():
print ("call function")
callFunc(Fun)
#函數(shù)作為返回值
def Func():
def subFunc():
print ("this is the inner function")
return subFunc
Func()
函數(shù)作為返回值的好處之一就是可以實(shí)現(xiàn)延遲計(jì)算,函數(shù)返回的只是一個(gè)功能函數(shù)并沒(méi)有將運(yùn)行結(jié)果返回假褪,所以當(dāng)年調(diào)用的時(shí)候署咽,就像是被人授漁而非授魚一樣(好別扭的典故引用),只有當(dāng)你需要的時(shí)候才去做計(jì)算生音。
0x02:高階函數(shù)
thinking like a Pythonor!
-
lambda 函數(shù)
寫過(guò)JavaScript的童鞋肯定知道匿名函數(shù)的作用了宁否,匿名函數(shù)可以直接使用,定義的地方就是作用的地方缀遍,但是你沒(méi)法在其他的地方調(diào)用它慕匠。因?yàn)椋麤](méi)有名字瑟由,你什么也不告訴編譯器絮重,編譯器也啥也不知道當(dāng)然不會(huì)調(diào)用了冤寿。匿名函數(shù)就是為了便利才設(shè)計(jì)的歹苦。
在Python中,你可以使用lambda函數(shù)來(lái)充當(dāng)匿名函數(shù)的角色督怜。lambda語(yǔ)句構(gòu)建的其實(shí)是一個(gè)函數(shù)對(duì)象殴瘦,因?yàn)槟憧梢詫⒑瘮?shù)賦值給一個(gè)變量,所以說(shuō)你可以使用變量來(lái)代替匿名函數(shù)來(lái)進(jìn)行調(diào)用号杠,當(dāng)然也可以結(jié)合其他的高級(jí)函數(shù)使用蚪腋。
#將匿名函數(shù)賦值給變量
p=lambda x : x+2
print (p(2))
p2=lambda x,y: x+y
print(p2(1,2))
這樣一看,lambda確實(shí)沒(méi)有什么大的作用嘛姨蟋,就是省略了函數(shù)名嘛屉凯。事實(shí)上就是這樣的,下面是些匿名函數(shù)的好處:
- 使用Python寫一些執(zhí)行腳本時(shí)眼溶,使用lambda可以省去定義函數(shù)的過(guò)程悠砚,讓代碼更加精簡(jiǎn)。
- 對(duì)于一些抽象的堂飞,不會(huì)別的地方再?gòu)?fù)用的函數(shù)灌旧,有時(shí)候給函數(shù)起個(gè)名字也是個(gè)難題绑咱,使用lambda不需要考慮命名的問(wèn)題。
- 使用lambda在某些時(shí)候讓代碼更容易理解枢泰。
-
map函數(shù)
map(function, iterable, ...)
Apply function to every item of iterable and return a list of the results. If additional iterable arguments are passed, function must take that many arguments and is applied to the items from all iterables in parallel. If one iterable is shorter than another it is assumed to be extended withNoneitems. If function isNone, the identity function is assumed; if there are multiple arguments, map() returns a list consisting of tuples containing the corresponding items from all iterables (a kind of transpose operation). The iterable arguments may be a sequence or any iterable object; the result is always a list.
先看最先一句:首先描融,你要有一個(gè)function(這個(gè)function就是改變iterable
data中的每個(gè)element的功能函數(shù),比如說(shuō)元素加倍衡蚂,元素取反)窿克。然后map函數(shù)會(huì)返回一個(gè)list給你。你看不到實(shí)際的循環(huán)過(guò)程讳窟,一切都是編譯器“偷偷”的完成的這些對(duì)數(shù)據(jù)的循環(huán)操作让歼。
def doubleMe(para):
return para*2
result=map(doubleMe,range(1,11))
#結(jié)果是result=[2,4,6,8,10,12,14,16,18,20]
#可以結(jié)合上面將的lambda函數(shù)直接寫成一行
result2=map(lambda x: x*2,range(1,11))
#結(jié)果是result2=[2,4,6,8,10,12,14,16,18,20],和上面的一致
你可以這么認(rèn)為丽啡,function是個(gè)操作機(jī)器手臂谋右,iterable data是個(gè)運(yùn)行中的傳送帶,data里面的每個(gè)element就是傳送帶上面的每一個(gè)材料补箍。第一句描述的是就是一個(gè)機(jī)器手臂改执,然后操作一條傳送帶(此時(shí)function只有一個(gè)參數(shù)),將每個(gè)element操作之后放在一個(gè)list中坑雅,完事之后推出這個(gè)list辈挂。
再看后面加了條件的一句:如果額外的iterable 參數(shù)需要傳遞的話,function函數(shù)需要同時(shí)去提取同位置的iterable data的元素裹粤。這一句描述的是還是一個(gè)機(jī)器手臂终蒂,但是現(xiàn)在有同時(shí)多個(gè)傳送帶在運(yùn)行(function有多個(gè)參數(shù)),機(jī)器手臂會(huì)同時(shí)從這些傳送帶上面取數(shù)據(jù)遥诉,然后進(jìn)行操作拇泣,最后還是推出list。
result=map(lambda x,y :x+y, range(1,5),range(6,10))
#結(jié)果是result=[7,9,11,13]
文檔重點(diǎn)描述的是矮锈,你需要同時(shí)從這些data中取相同下標(biāo)的element霉翔。后面的一句,說(shuō)的是假設(shè)你其中的一個(gè)data比另一個(gè)data包含的數(shù)據(jù)少苞笨,那么你會(huì)自動(dòng)將短的list填充Noneitems.
result=map(lambda x,y :x+y, range(1,6),range(6,10))
#結(jié)果是會(huì)報(bào)錯(cuò)誤:TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
再看最后一種情況:假如我們傳遞給map的是一個(gè)None的function會(huì)怎么樣呢债朵,此時(shí)什么也不會(huì)發(fā)生,實(shí)際上就是做了個(gè)數(shù)據(jù)轉(zhuǎn)移操作瀑凝,將數(shù)據(jù)從iterable data轉(zhuǎn)移到list中序芦。
map(None,range(1,3))
#輸出 [1,2]
map(None,range(1,3),range(5,7))
#輸出[(1, 5), (2, 6)]
#可以看到第二個(gè)有多個(gè)參數(shù)組的情況下,只是把同下標(biāo)的給“打包”了
總結(jié):這些高級(jí)函數(shù)在高手手中真的是能省下不少力氣啊粤咪,好處是消除了for 循環(huán)的“邊界效應(yīng)“谚中,同時(shí),和lambda函數(shù)同時(shí)使用,確實(shí)能讓人很好的閱讀和理解藏杖。像我這種從命令式語(yǔ)言入門的程序員來(lái)說(shuō)将塑,確實(shí)剛接觸的時(shí)候很吃驚也很費(fèi)解。其實(shí)拋開你對(duì)命令式語(yǔ)言的執(zhí)念蝌麸,這些更貼近數(shù)學(xué)和”白話“的語(yǔ)句真的很好理解了点寥。如果你不懂什么是命令式編程和函數(shù)式編程,這一篇《00.編程學(xué)習(xí)--初始》是從語(yǔ)言模型上講述兩者的區(qū)別来吩,可以參看下敢辩。
-
reduce函數(shù)
reduce(function, iterable[, initializer])
Apply function of two arguments cumulatively to the items of iterable, from left to right, so as to reduce the iterable to a single value. If the optional initializer is present, it is placed before the items of the iterable in the calculation, and serves as a default when the iterable is empty. If initializer is not given and iterable contains only one item, the first item is returned.
map函數(shù)講述的是對(duì)傳送帶上的數(shù)據(jù)做操作之后會(huì)放到一個(gè)list里面,也就是結(jié)果的個(gè)數(shù)和傳送帶(不管有幾個(gè)傳送帶在工作)上面的數(shù)據(jù)個(gè)數(shù)是一致的弟疆。同時(shí)傳送帶上面的零件都是一樣的戚长,比如都是汽車車門,我們的操作可以是噴漆或是焊接怠苔。有的時(shí)候我們有個(gè)機(jī)器手臂是做組裝的,而且傳送帶上面的零件都是不同類型的柑司,比如是車門、車架攒驰、車輪、發(fā)動(dòng)機(jī)之類的玻粪,最后需要組裝在一起。這里就可以用reduce函數(shù)了蹲缠,他的返回是只是一個(gè)(并不是list)。
第一種:將iterable data里面的數(shù)據(jù),從左到右,挨個(gè)取出來(lái)放到function里
矢洲,function必須接受兩個(gè)參數(shù)泰演,一個(gè)就是取出來(lái)的這個(gè)元素,一個(gè)就是前面每次操作的結(jié)果偎窘。reduce的本意是消除仆葡,這里的意思就是將一大堆的數(shù)據(jù)挨個(gè)消除,最后整合(流水線裝配)在一起呜袁。
result = reduce(lambda x,y : x+y, range(1,5))
#結(jié)果是result=10
第二種:有的時(shí)候我們可以傳遞第三個(gè)參數(shù)祭玉,這是一個(gè)初始值,就像是有的時(shí)候前面的裝配線已經(jīng)裝了部分汽車了料仗,現(xiàn)在的裝配線(reduce)只需要裝內(nèi)飾就行伏蚊。如果沒(méi)有初始值的話立轧,就使用iterable data的第一個(gè)element作為初始值。
result = reduce(lambda x,y : x+y, range(1,5),3)
#結(jié)果是result=13
-
filter函數(shù)
filter(function, iterable)
Construct a list from those elements of iterable for which function returns true. iterable may be either a sequence, a container which supports iteration, or an iterator. If iterable is a string or a tuple, the result also has that type; otherwise it is always a list. If function is None, the identity function is assumed, that is, all elements of iterable that are false are removed.
還是用生產(chǎn)線的例子講解帐萎,有的時(shí)候我們需要嚴(yán)格把控質(zhì)量胜卤,需要將不合格的零件挑選出來(lái),這時(shí)候就需要filter了(filter本意就是篩選嘛)葛躏。filter函數(shù)的作用就是將iterable data里面符合你function的element選出來(lái)。你的function必須返回True/False败富。如果是你的function是None的話摩窃,那么編譯器默認(rèn)的function就會(huì)將iterable data 中值是False的element去除。
result = filter(lambda x: x>3, range(1,5))
#結(jié)果是result=[4]
result = filter(None,range(1,3))
#結(jié)果是result=[1,2]
result=filter(None,[False,True])
# 結(jié)果是result=[True]
總結(jié)
其實(shí)這就是Python中”流水線”操作猾愿,你可以想象成汽車裝配廠的流水線。
map 函數(shù):噴漆流水線(可以有多條)
reduce 函數(shù):組裝流水線(只有一條)
filter 函數(shù):篩選流水線(只有一條)