函數(shù)式編程
函數(shù)式編程這個概念我們可能或多或少都聽說過,剛聽說的時候不明覺厲,覺得這是一個非常黑科技的概念。但是實際上它的含義很樸實,但是延伸出來許多豐富的用法。
在早期編程語言還不是很多的時候,我們會將語言分成高級語言與低級語言。比如匯編語言,就是低級語言,幾乎什么封裝也沒有,做一個賦值運算還需要我們手動調(diào)用寄存器。而高級語言則從這些面向機器的指令當(dāng)中抽身出來,轉(zhuǎn)而面向過程或者是對象。也就是說我們寫代碼面向的是一段計算過程或者是一個計算機當(dāng)中抽象出來的對象。如果你學(xué)過面向?qū)ο?,你會發(fā)現(xiàn)和面向過程相比,面向?qū)ο蟮某橄蟪潭雀吡艘恍?,做了更加完善的封裝。
在面向?qū)ο笾竽兀覀冞€可以做什么封裝和抽象呢?這就輪到了函數(shù)式編程。
函數(shù)我們都了解,就是我們定義的一段程序,它的輸入和輸出都是確定的。我們把一段函數(shù)寫好,它可以在任何地方進行調(diào)用。既然函數(shù)這么好用,那么能不能把函數(shù)也看成是一個變量進行返回和傳參呢?
OK,這個就是函數(shù)式編程最直觀的特點。也就是說我們寫的一段函數(shù)也可以作為變量,既可以用來賦值,還可以用來傳遞,并且還能進行返回。這樣一來,大大方便了我們的編碼,但是這并不是有利無害的,相反它帶來許多問題,最直觀的問題就是由于函數(shù)傳入的參數(shù)還可以是另一個函數(shù),這會導(dǎo)致函數(shù)的計算過程變得不可確定,許多超出我們預(yù)期的事情都有可能發(fā)生。
所以函數(shù)式編程是有利有弊的,它的確簡化了許多問題,但也產(chǎn)生了許多新的問題,我們在使用的過程當(dāng)中需要謹慎。
傳入、返回函數(shù)在我們之前介紹filter、map、reduce以及自定義排序的時候,其實我們已經(jīng)用到了函數(shù)式編程的概念了。
比如在我們調(diào)用sorted進行排序的時候,如果我們傳入的是一個對象數(shù)組,我們希望根據(jù)我們制定的字段排序,這個時候我們往往需要傳入一個匿名函數(shù),用來制定排序的字段。其實傳入的匿名函數(shù),其實就是函數(shù)式編程最直觀的體現(xiàn)了:
sorted(kids,key=lambdax:x['score'])
除此之外,我們還可以返回一個函數(shù),比如我們來看一個例子:
defdelay_sum(nums):
defsum():
s=0
foriinnums:
s+=i
returns
returnsum
如果這個時候我們調(diào)用delay_sum傳入一串?dāng)?shù)字,我們會得到什么?
答案是一個函數(shù),我們可以直接輸出,從打印信息里看出這一點:
>>>delay_sum([1,3,4,2])
.sumat0x1018659e0>
我們想獲得這個運算結(jié)果應(yīng)該怎么辦呢?也很簡單,我們用一個變量去接收它,然后執(zhí)行這個新的變量即可:
>>>f=delay_sum([1,3,4,2])
>>>f()
10
這樣做有一個好處是我們可以延遲計算,如果不使用函數(shù)式編程,那么我們需要在調(diào)用delay_sum這個函數(shù)的時候就計算出結(jié)果。如果這個運算量很小還好,如果這個運算量很大,就會造成開銷。并且當(dāng)我們計算出結(jié)果來之后,這個結(jié)果也許不是立即使用的,可能到很晚才會用到。既然如此,我們返回一個函數(shù)代替了運算,當(dāng)后面真正需要用到的時候再執(zhí)行結(jié)果,從而延遲了運算。這也是很多計算框架的常用思路,比如spark。
閉包我們再來回顧一下我們剛才舉的例子,在剛才的delay_sum函數(shù)當(dāng)中,我們內(nèi)部實現(xiàn)了一個sum函數(shù),我們在這個函數(shù)當(dāng)中調(diào)用了delay_sum函數(shù)傳入的參數(shù)。這種對外部作用域的變量進行引用的內(nèi)部函數(shù)就稱為閉包。
其實這個概念很形象,因為這個函數(shù)內(nèi)部調(diào)用的數(shù)據(jù)對于調(diào)用方來說是封閉的,完全是一個黑盒,除非我們查看源碼,否則我們是不知道它當(dāng)中數(shù)據(jù)的來源的。除了不知道來源之外,更重要的是它引用的是外部函數(shù)的變量,既然是變量就說明是動態(tài)的。也就是說我們可以通過改變某些外部變量的值來改變閉包的運行效果。
這么說有點拗口,我們來看一個簡單的例子。在Python當(dāng)中有一個函數(shù)叫做math.pow其實就是計算次方的。比如我們要計算x的平方,那么我們應(yīng)該這樣寫:
math.pow(x,2)
但是如果我們當(dāng)前場景下只需要計算平方,我們每次都要傳入額外再傳入一個2會顯得非常麻煩,這個時候我們使用閉包,可以簡化操作:
defmypow(num):
defpw(x):
returnmath.pow(x,num)
returnpw
pow2=mypow(2)
print(pow2(10))
通過閉包,我們把第二個變量給固定了,這樣我們只需要使用pow2就可以實現(xiàn)原來math.pow(x,2)的功能了。如果我們突然需求變更需要計算3次方或者是4次方,我們只需要修改mypow的傳入?yún)?shù)即可,完全不需要修改代碼。
實際上這也是閉包最大的使用場景,我們可以通過閉包實現(xiàn)一些非常靈活的功能,以及通過配置修改一些功能等操作,而不再需要通過代碼寫死。要知道對于工業(yè)領(lǐng)域來說,線上的代碼是不能隨便變更的,尤其是客戶端,比如applestore或者是安卓商店當(dāng)中的軟件包,只有用戶手動更新才會拉取。如果出現(xiàn)問題了,幾乎沒有辦法修改,只能等用戶手動更新。所以常規(guī)操作就是使用一些類似閉包的靈活功能,通過修改配置的方式改變代碼的邏輯。
除此之外閉包還有一個用處是可以暫存變量或者是運行時的環(huán)境。
舉個例子,我們來看下面這段代碼:
defstep(x=0):
x+=5
returnx
這是沒有使用閉包的函數(shù),不管我們調(diào)用多少次,答案都是5,執(zhí)行完x+=5之后的結(jié)果并不會被保存起來,當(dāng)函數(shù)返回了,這個暫存的值也就被拋棄了。那如果我希望每次調(diào)用都是依據(jù)上次調(diào)用的結(jié)果,也就是說我們每次修改的操作都能保存起來,而不是丟棄呢?
這個時候就需要使用閉包了:
deftest(x=0):
defstep():
nonlocalx
x+=5
returnx
returnstep
t=test()
t()
>>>5
t()
>>>10
也就是說我們的x的值被存儲起來了,每次修改都會累計,而不是丟棄。這里需要注意一點,我們用到了一個新的關(guān)鍵字叫做nonlocal,這是Python3當(dāng)中獨有的關(guān)鍵字,用來申明當(dāng)前的變量x不是局部變量,這樣Python解釋器就會去全局變量當(dāng)中去尋找這個x,這樣就能關(guān)聯(lián)上test方法當(dāng)中傳入的參數(shù)x。Python2官方已經(jīng)不更新了,不推薦使用。
由于在Python當(dāng)中也是一切都是對象,如果我們把閉包外層的函數(shù)看成是一個類的話,其實閉包和類區(qū)別就不大了,我們甚至可以給閉包返回的函數(shù)關(guān)聯(lián)函數(shù),這樣幾乎就是一個對象了。來看一個例子:
defstudent():
name='xiaoming'
defstu():
returnname
defset_name(value):
nonlocalname
name=value
stu.set_name=set_name
returnstu
stu=student()
stu.set_name('xiaohong')
print(stu())
最后運算的結(jié)果是xiaohong,因為我們調(diào)用set_name改變了閉包外部的值。這樣當(dāng)然是可以的,但是一般情況下我們并不會用到它。和寫一個class相比,通過閉包的方法運算速度會更快。原因比較隱蔽,是因為閉包當(dāng)中沒有self指針,從而節(jié)省了大量的變量的訪問和運算,所以計算的速度要快上一些。但是閉包搞出來的偽對象是不能使用繼承、派生等方法的,而且和正常的用法格格不入,所以我們知道有這樣的方法就可以了,現(xiàn)實中并不會用到。
閉包的坑閉包雖然好用,但是不小心的話也是很容易踩坑的,下面介紹幾個常見的坑點。
閉包不能直接訪問外部變量
這一點我們剛才已經(jīng)提到了,在閉包當(dāng)中我們不能直接訪問外部的變量的,必須要通過nonlocal關(guān)鍵字進行標注,否則的話是會報錯的。
deftest():
n=0
deft():
n+=5
returnn
returnt
比如這樣的話,就會報錯:
閉包當(dāng)中不能使用循環(huán)變量
閉包有一個很大的問題就是不能使用循環(huán)變量,這個坑藏得很深,因為單純從代碼的邏輯上來看是發(fā)現(xiàn)不了的。也就是說邏輯上沒問題的代碼,運行的時候往往會出乎我們的意料,這需要我們對底層的原理有深刻地了解才能發(fā)現(xiàn),比如我們來看一個例子:
deftest(x):
fs=[]
foriinrange(3):
deff():
returnx+i
fs.append(f)
returnfs
fs=test(3)
forfinfs:
print(f())
在上面這個例子當(dāng)中,我們使用了for循環(huán)來創(chuàng)建了3個閉包,我們使用fs存儲這三個閉包并進行返回。然后我們通過調(diào)用test,來獲得了這3個閉包,然后我們進行了調(diào)用。
這個邏輯看起來應(yīng)該沒有問題,按照道理,這3個閉包是通過for循環(huán)創(chuàng)建的,并且在閉包當(dāng)中我們用到了循環(huán)變量i。那按照我們的想法,最終輸出的結(jié)果應(yīng)該是[3,4,5],但是很遺憾,最后我們得到的結(jié)果是[5,5,5]。
看起來很奇怪吧,其實一點也不奇怪,因為循環(huán)變量i并不是在創(chuàng)建閉包的時候就set好的。而是當(dāng)我們執(zhí)行閉包的時候,我們再去尋找這個i對應(yīng)的取值,顯然當(dāng)我們運行閉包的時候,循環(huán)已經(jīng)執(zhí)行完了,此時的i停在了2。所以這3個閉包的執(zhí)行結(jié)果都是2+3也就是5。這個坑是由Python解釋器當(dāng)中對于閉包執(zhí)行的邏輯導(dǎo)致的,我們編寫的邏輯是對的,但是它并不按照我們的邏輯來,所以這一點要千萬注意,如果忘記了,想要通過debug查找出來會很難。
總結(jié)雖然從表面上閉包存在一些問題和坑點,但是它依然是我們經(jīng)常使用的Python高級特性,并且它也是很多其他高級用法的基礎(chǔ)。所以我們理解和學(xué)會閉包是非常有必要的,千萬不能因噎廢食。
其實并不只是閉包,很多高度抽象的特性都或多或少的有這樣的問題。因為當(dāng)我們進行抽象的時候,我們固然簡化了代碼,增加了靈活度,但與此同時我們也讓學(xué)習(xí)曲線變得陡峭,帶來了更多我們需要理解和記住的內(nèi)容。本質(zhì)上這也是一個trade-off,好用的特性需要付出代碼,易學(xué)易用的往往意味著比較死板不夠靈活。對于這個問題,我們需要保持心態(tài),不過好在初看時也許有些難以理解,但總體來說閉包還是比較簡單的,我相信對你們來說一定不成問題。
以上內(nèi)容為大家介紹了Python的函數(shù)式編程與閉包,希望對大家有所幫助,如果想要了解更多Python相關(guān)知識,請關(guān)注IT培訓(xùn)機構(gòu):千鋒教育。