一.什么是函數(shù)式編程避诽?
函數(shù)式編程使用一系列的函數(shù)解決問題恼除。函數(shù)僅接受輸入并產(chǎn)生輸出兢仰,不包含任何能影響產(chǎn)生輸出的內(nèi)部狀態(tài)嬉愧。任何情況下贩挣,使用相同的參數(shù)調(diào)用函數(shù)始終能產(chǎn)生同樣的結(jié)果。
在一個函數(shù)式的程序中没酣,輸入的數(shù)據(jù)“流過”一系列的函數(shù)王财,每一個函數(shù)根據(jù)它的輸入產(chǎn)生輸出。函數(shù)式風(fēng)格避免編寫有“邊界效應(yīng)”(side effects)的函數(shù):修改內(nèi)部狀態(tài)裕便,或者是其他無法反應(yīng)在輸出上的變化佑稠。完全沒有邊界效應(yīng)的函數(shù)被稱為“純函數(shù)式的”(purely functional)朗若。避免邊界效應(yīng)意味著不使用在程序運行時可變的數(shù)據(jù)結(jié)構(gòu),輸出只依賴于輸入耻警。
可以認(rèn)為函數(shù)式編程剛好站在了面向?qū)ο缶幊痰膶α⒚婷誊浴ο笸ǔ0瑑?nèi)部狀態(tài)(字段)阱冶,和許多能修改這些狀態(tài)的函數(shù)顽冶,程序則由不斷修改狀態(tài)構(gòu)成采驻;函數(shù)式編程則極力避免狀態(tài)改動,并通過在函數(shù)間傳遞數(shù)據(jù)流進行工作视事。但這并不是說無法同時使用函數(shù)式編程和面向?qū)ο缶幊棠校聦嵣希瑥?fù)雜的系統(tǒng)一般會采用面向?qū)ο蠹夹g(shù)建模郑口,但混合使用函數(shù)式風(fēng)格還能讓你額外享受函數(shù)式風(fēng)格的優(yōu)點。
為什么使用函數(shù)式編程:
函數(shù)式的風(fēng)格通常被認(rèn)為有如下優(yōu)點:
- 邏輯可證 這是一個學(xué)術(shù)上的優(yōu)點:沒有邊界效應(yīng)使得更容易從邏輯上證明程序是正確的(而不是通過測試)盾鳞。
- 模塊化 函數(shù)式編程推崇簡單原則犬性,一個函數(shù)只做一件事情,將大的功能拆分成盡可能小的模塊腾仅。小的函數(shù)更易于閱讀和檢查錯誤乒裆。
- 組件化 小的函數(shù)更容易加以組合形成新的功能。
易于調(diào)試 細(xì)化的推励、定義清晰的函數(shù)使得調(diào)試更加簡單鹤耍。當(dāng)程序不正常運行時,每一個函數(shù)都是檢查數(shù)據(jù)是否正確的接口验辞,能更快速地排除沒有問題的代碼稿黄,定位到出現(xiàn)問題的地方。 - 易于測試 不依賴于系統(tǒng)狀態(tài)的函數(shù)無須在測試前構(gòu)造測試樁跌造,使得編寫單元測試更加容易杆怕。
- 更高的生產(chǎn)率 函數(shù)式編程產(chǎn)生的代碼比其他技術(shù)更少(往往是其他技術(shù)的一半左右)族购,并且更容易閱讀和維護。
下面依次介紹一些函數(shù)式編程的特性:
二.高階函數(shù)
高階函數(shù)英文叫Higher-order function陵珍。什么是高階函數(shù)寝杖?
- 變量可以指向函數(shù)
如abs
是求絕對值的函數(shù),如下:
>>>f = abs
>>>f
<built-in function abs>
由上可見函數(shù)本身也可以賦值給變量,即:變量可以指向函數(shù)互纯。并且調(diào)用也是可以的
>>>f(-2))
2
- 函數(shù)名也是變量
函數(shù)名是什么呢瑟幕?函數(shù)名其實就是指向函數(shù)的變量!對于abs()這個函數(shù)留潦,完全可以把函數(shù)名abs看成變量只盹,它指向一個可以計算絕對值的函數(shù)!
如果把abs指向其他對象愤兵,會有什么情況發(fā)生鹿霸?
>>> abs = 10
>>> abs(-10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not callable
可以發(fā)現(xiàn)把abs指向10后,就無法通過abs(-10)調(diào)用該函數(shù)了秆乳!因為abs這個變量已經(jīng)不指向求絕對值函數(shù)而是指向一個整數(shù)10懦鼠!
當(dāng)然實際代碼絕對不能這么寫,這里是為了說明函數(shù)名也是變量屹堰。要恢復(fù)abs函數(shù)肛冶,需要重啟Python交互環(huán)境。
- 傳入函數(shù)
既然變量可以指向函數(shù)扯键,函數(shù)的參數(shù)能接收變量睦袖,那么一個函數(shù)就可以接收另一個函數(shù)作為參數(shù),這種函數(shù)就稱之為高階函數(shù)荣刑。如下一個最簡單的高階函數(shù):
def add(x, y, f):
return f(x) + f(y)
用代碼驗證一下:
>>> add(-5, 6, abs)
11
一些常見的高階函數(shù)
-
map
map函數(shù)是對一個序列的每個項依次執(zhí)行函數(shù)馅笙,下面是對一個序列每個項都乘以2:
>>>a=map(lambda x:x*x,[1,2,3])
>>>list(a)
[1, 4, 9]
-
reduce
reduce函數(shù)是對一個序列的每個項迭代調(diào)用函數(shù),下面是求3的階乘:
>>>from functools import reduce
>>>b=reduce (lambda x,y:x*y ,range(1,4))
>>>b
6
用reduce就可以用一行代碼寫出階乘函數(shù),如下:
>>>from functools import reduce
>>>g=lambda x: reduce (lambda a,b:a*b ,range(1,x+1))
>>>g(4)
24
-
filter
Python內(nèi)建的filter()函數(shù)用于過濾序列厉亏。
和map()類似董习,filter()也接收一個函數(shù)和一個序列。和map()不同的是爱只,filter()把傳入的函數(shù)依次作用于每個元素皿淋,然后根據(jù)返回值是True還是False決定保留還是丟棄該元素
例如,在一個list中恬试,刪掉偶數(shù)窝趣,只保留奇數(shù),可以這么寫:
def is_odd(n):
return n % 2 == 1
list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15]))
# 結(jié)果: [1, 5, 9, 15]
三.匿名函數(shù)
lambda是匿名函數(shù)训柴,沒有函數(shù)名哑舒,在閉包里和map reduce中很好用。
lambda提供了快速編寫簡單函數(shù)的能力畦粮。對于偶爾為之的行為散址,lambda讓你不再需要在編碼時跳轉(zhuǎn)到其他位置去編寫函數(shù)乖阵。 lambda表達式定義一個匿名的函數(shù),如果這個函數(shù)僅在編碼的位置使用到预麸,你可以現(xiàn)場定義瞪浸、直接使用
如下面是求兩數(shù)乘積的函數(shù):
lst.sort(lambda o1, o2: o1.compareTo(o2))
相信從這個小小的例子你也能感受到強大的生產(chǎn)效率
四.閉包
閉包就是函數(shù)中又定義函數(shù),里面的函數(shù)可以使用外部作用域內(nèi)的變量吏祸,但不是全局變量对蒲,所以一次創(chuàng)建外部函數(shù),則多次調(diào)用內(nèi)部函數(shù)時外部作用域內(nèi)的變量還是有效的贡翘;多次運行外部函數(shù)蹈矮,則會重新創(chuàng)建閉包,原來外部變量的值也不會受到影響鸣驱,如上例可以實現(xiàn)每次調(diào)用內(nèi)部函數(shù)是的外部變量+1操作泛鸟。這里說一下作用域的事兒,原來的python2只有全局作用域和局部作用域踊东,python3新增的nonlocal作用域就是專門針對閉包的北滥,nonlocal聲明的變量只能訪問外部作用域的變量,不能訪問全局作用域的變量闸翅,這樣就不用全局變量搞來搞去錯都不知道哪里出的了再芋。
如下所示:
def funA():
x=0
def funB():
nonlocal x
x+=1
print (x)
return funB
a=funA()
a()
# 輸出1
a()
# 輸出2
如果再定義一個b=funA()
,會得到一個新的funB,不會影響之前的閉包的值
a=funA()
a()
# 1
a()
# 2
b=funA()
b()
# 1
a()
# 3
可見,閉包在不影響可讀性的同時也省下了不少代碼量坚冀。
五.其他特性
- 避免使用變量
為了避開邊界效應(yīng)济赎,函數(shù)式風(fēng)格盡量避免使用變量,而僅僅為了控制流程而定義的循環(huán)變量和流程中產(chǎn)生的臨時變量無疑是最需要避免的记某。 假如我們需要對剛才的數(shù)集進行過濾得到所有的正數(shù)司训,使用指令式風(fēng)格的代碼應(yīng)該像是這樣:
lst2 = list()
for i in range(len(lst)): #模擬經(jīng)典for循環(huán)
if lst[i] > 0:
lst2.append(lst[i])
這段代碼把從創(chuàng)建新列表、循環(huán)液南、取出元素豁遭、判斷、添加至新列表的整個流程完整的展示了出來贺拣,儼然把解釋器當(dāng)成了需要手把手指導(dǎo)的傻瓜。然而捂蕴,“過濾”這個動作是很常見的譬涡,為什么解釋器不能掌握過濾的流程,而我們只需要告訴它過濾規(guī)則呢啥辨? 在Python里涡匀,過濾由一個名為filter的內(nèi)置函數(shù)實現(xiàn)。有了這個函數(shù)溉知,解釋器就學(xué)會了如何“過濾”陨瘩,而我們只需要把規(guī)則告訴它:
lst2 = filter(lambda n: n > 0, lst)
這個函數(shù)帶來的好處不僅僅是少寫了幾行代碼這么簡單腕够。 封裝控制結(jié)構(gòu)后,代碼中就只需要描述功能而不是做法舌劳,這樣的代碼更清晰帚湘,更可讀。因為避開了控制結(jié)構(gòu)的干擾甚淡,第二段代碼顯然能讓你更容易了解它的意圖大诸。 另外,因為避開了索引贯卦,使得代碼中不太可能觸發(fā)下標(biāo)越界這種異常资柔,除非你手動制造一個。 函數(shù)式編程語言通常封裝了數(shù)個類似“過濾”這樣的常見動作作為模板函數(shù)撵割。唯一的缺點是這些函數(shù)需要少量的學(xué)習(xí)成本贿堰,但這絕對不能掩蓋使用它們帶來的好處。
- 內(nèi)置的不可變數(shù)據(jù)結(jié)構(gòu)
為了避開邊界效應(yīng)啡彬,不可變的數(shù)據(jù)結(jié)構(gòu)是函數(shù)式編程中不可或缺的部分羹与。不可變的數(shù)據(jù)結(jié)構(gòu)保證數(shù)據(jù)的一致性,極大地降低了排查問題的難度外遇。 例如注簿,Python中的元組(tuple)就是不可變的,所有對元組的操作都不能改變元組的內(nèi)容跳仿,所有試圖修改元組內(nèi)容的操作都會產(chǎn)生一個異常诡渴。 函數(shù)式編程語言一般會提供數(shù)據(jù)結(jié)構(gòu)的兩種版本(可變和不可變),并推薦使用不可變的版本菲语。