今天是Python專題的第9篇文章,咱們來聊聊Python的函數式編程與閉包。編程
函數式編程這個概念咱們可能或多或少都據說過,剛據說的時候不明覺厲,以爲這是一個很是黑科技的概念。可是實際上它的含義很樸實,可是延伸出來許多豐富的用法。數組
在早期編程語言還不是不少的時候,咱們會將語言分紅高級語言與低級語言。好比彙編語言,就是低級語言,幾乎什麼封裝也沒有,作一個賦值運算還須要咱們手動調用寄存器。而高級語言則從這些面向機器的指令當中抽身出來,轉而面向過程或者是對象。也就是說咱們寫代碼面向的是一段計算過程或者是一個計算機當中抽象出來的對象。若是你學過面向對象,你會發現和麪向過程相比,面向對象的抽象程度更高了一些,作了更加完善的封裝。閉包
在面向對象以後呢,咱們還能夠作什麼封裝和抽象呢?這就輪到了函數式編程。app
函數咱們都瞭解,就是咱們定義的一段程序,它的輸入和輸出都是肯定的。咱們把一段函數寫好,它能夠在任何地方進行調用。既然函數這麼好用,那麼能不能把函數也當作是一個變量進行返回和傳參呢?框架
OK,這個就是函數式編程最直觀的特色。也就是說咱們寫的一段函數也能夠做爲變量,既能夠用來賦值,還能夠用來傳遞,而且還能進行返回。這樣一來,大大方便了咱們的編碼,可是這並非有利無害的,相反它帶來許多問題,最直觀的問題就是因爲函數傳入的參數還能夠是另外一個函數,這會致使函數的計算過程變得不可肯定,許多超出咱們預期的事情都有可能發生。編程語言
因此函數式編程是有利有弊的,它的確簡化了許多問題,但也產生了許多新的問題,咱們在使用的過程中須要謹慎。ide
在咱們以前介紹filter、map、reduce以及自定義排序的時候,其實咱們已經用到了函數式編程的概念了。函數式編程
好比在咱們調用sorted進行排序的時候,若是咱們傳入的是一個對象數組,咱們但願根據咱們制定的字段排序,這個時候咱們每每須要傳入一個匿名函數,用來制定排序的字段。其實傳入的匿名函數,其實就是函數式編程最直觀的體現了:函數
sorted(kids, key=lambda x: x['score'])
除此以外,咱們還能夠返回一個函數,好比咱們來看一個例子:學習
def delay_sum(nums):
def sum():
s = 0
for i in nums:
s += i
return s
return sum
若是這個時候咱們調用delay_sum傳入一串數字,咱們會獲得什麼?
答案是一個函數,咱們能夠直接輸出,從打印信息裏看出這一點:
>>> delay_sum([1, 3, 4, 2])
<function delay_sum.<locals>.sum at 0x1018659e0>
咱們想得到這個運算結果應該怎麼辦呢?也很簡單,咱們用一個變量去接收它,而後執行這個新的變量便可:
>>> f = delay_sum([1, 3, 4, 2])
>>> f()
10
這樣作有一個好處是咱們能夠延遲計算,若是不使用函數式編程,那麼咱們須要在調用delay_sum這個函數的時候就計算出結果。若是這個運算量很小還好,若是這個運算量很大,就會形成開銷。而且當咱們計算出結果來以後,這個結果也許不是當即使用的,可能到很晚纔會用到。既然如此,咱們返回一個函數代替了運算,當後面真正須要用到的時候再執行結果,從而延遲了運算。這也是不少計算框架的經常使用思路,好比spark。
咱們再來回顧一下咱們剛纔舉的例子,在剛纔的delay_sum函數當中,咱們內部實現了一個sum函數,咱們在這個函數當中調用了delay_sum函數傳入的參數。這種對外部做用域的變量進行引用的內部函數就稱爲閉包。
其實這個概念很形象,由於這個函數內部調用的數據對於調用方來講是封閉的,徹底是一個黑盒,除非咱們查看源碼,不然咱們是不知道它當中數據的來源的。除了不知道來源以外,更重要的是它引用的是外部函數的變量,既然是變量就說明是動態的。也就是說咱們能夠經過改變某些外部變量的值來改變閉包的運行效果。
這麼說有點拗口,咱們來看一個簡單的例子。在Python當中有一個函數叫作math.pow其實就是計算次方的。好比咱們要計算x的平方,那麼咱們應該這樣寫:
math.pow(x, 2)
可是若是咱們當前場景下只須要計算平方,咱們每次都要傳入額外再傳入一個2會顯得很是麻煩,這個時候咱們使用閉包,能夠簡化操做:
def mypow(num):
def pw(x):
return math.pow(x, num)
return pw
pow2 = mypow(2)
print(pow2(10))
經過閉包,咱們把第二個變量給固定了,這樣咱們只須要使用pow2就能夠實現原來math.pow(x, 2)的功能了。若是咱們忽然需求變動須要計算3次方或者是4次方,咱們只須要修改mypow的傳入參數便可,徹底不須要修改代碼。
實際上這也是閉包最大的使用場景,咱們能夠經過閉包實現一些很是靈活的功能,以及經過配置修改一些功能等操做,而再也不須要經過代碼寫死。要知道對於工業領域來講,線上的代碼是不能隨便變動的,尤爲是客戶端,好比apple store或者是安卓商店當中的軟件包,只有用戶手動更新纔會拉取。若是出現問題了,幾乎沒有辦法修改,只能等用戶手動更新。因此常規操做就是使用一些相似閉包的靈活功能,經過修改配置的方式改變代碼的邏輯。
除此以外閉包還有一個用處是能夠暫存變量或者是運行時的環境。
舉個例子,咱們來看下面這段代碼:
def step(x=0):
x += 5
return x
這是沒有使用閉包的函數,無論咱們調用多少次,答案都是5,執行完x+=5以後的結果並不會被保存起來,當函數返回了,這個暫存的值也就被拋棄了。那若是我但願每次調用都是依據上次調用的結果,也就是說咱們每次修改的操做都能保存起來,而不是丟棄呢?
這個時候就須要使用閉包了:
def test(x=0):
def step():
nonlocal x
x += 5
return x
return step
t = test()
t()
>>> 5
t()
>>> 10
也就是說咱們的x的值被存儲起來了,每次修改都會累計,而不是丟棄。這裏須要注意一點,咱們用到了一個新的關鍵字叫作nonlocal,這是Python3當中獨有的關鍵字,用來申明當前的變量x不是局部變量,這樣Python解釋器就會去全局變量當中去尋找這個x,這樣就能關聯上test方法當中傳入的參數x。Python2官方已經不更新了,不推薦使用。
因爲在Python當中也是一切都是對象,若是咱們把閉包外層的函數當作是一個類的話,其實閉包和類區別就不大了,咱們甚至能夠給閉包返回的函數關聯函數,這樣幾乎就是一個對象了。來看一個例子:
def student():
name = 'xiaoming'
def stu():
return name
def set_name(value):
nonlocal name
name = value
stu.set_name = set_name
return stu
stu = student()
stu.set_name('xiaohong')
print(stu())
最後運算的結果是xiaohong,由於咱們調用set_name改變了閉包外部的值。這樣固然是能夠的,可是通常狀況下咱們並不會用到它。和寫一個class相比,經過閉包的方法運算速度會更快。緣由比較隱蔽,是由於閉包當中沒有self指針,從而節省了大量的變量的訪問和運算,因此計算的速度要快上一些。可是閉包搞出來的僞對象是不能使用繼承、派生等方法的,並且和正常的用法格格不入,因此咱們知道有這樣的方法就能夠了,現實中並不會用到。
閉包雖然好用,可是不當心的話也是很容易踩坑的,下面介紹幾個常見的坑點。
這一點咱們剛纔已經提到了,在閉包當中咱們不能直接訪問外部的變量的,必需要經過nonlocal關鍵字進行標註,不然的話是會報錯的。
def test():
n = 0
def t():
n += 5
return n
return t
好比這樣的話,就會報錯:
閉包有一個很大的問題就是不能使用循環變量,這個坑藏得很深,由於單純從代碼的邏輯上來看是發現不了的。也就是說邏輯上沒問題的代碼,運行的時候每每會出乎咱們的意料,這須要咱們對底層的原理有深入地瞭解才能發現,好比咱們來看一個例子:
def test(x):
fs = []
for i in range(3):
def f():
return x + i
fs.append(f)
return fs
fs = test(3)
for f in fs:
print(f())
在上面這個例子當中,咱們使用了for循環來建立了3個閉包,咱們使用fs存儲這三個閉包並進行返回。而後咱們經過調用test,來得到了這3個閉包,而後咱們進行了調用。
這個邏輯看起來應該沒有問題,按照道理,這3個閉包是經過for循環建立的,而且在閉包當中咱們用到了循環變量i。那按照咱們的想法,最終輸出的結果應該是[3, 4, 5],可是很遺憾,最後咱們獲得的結果是[5, 5, 5]。
看起來很奇怪吧,其實一點也不奇怪,由於循環變量i並非在建立閉包的時候就set好的。而是當咱們執行閉包的時候,咱們再去尋找這個i對應的取值,顯然當咱們運行閉包的時候,循環已經執行完了,此時的i停在了2。因此這3個閉包的執行結果都是2+3也就是5。這個坑是由Python解釋器當中對於閉包執行的邏輯致使的,咱們編寫的邏輯是對的,可是它並不按照咱們的邏輯來,因此這一點要千萬注意,若是忘記了,想要經過debug查找出來會很難。
雖然從表面上閉包存在一些問題和坑點,可是它依然是咱們常用的Python高級特性,而且它也是不少其餘高級用法的基礎。因此咱們理解和學會閉包是很是有必要的,千萬不能因噎廢食。
其實並不僅是閉包,不少高度抽象的特性都或多或少的有這樣的問題。由於當咱們進行抽象的時候,咱們當然簡化了代碼,增長了靈活度,但與此同時咱們也讓學習曲線變得陡峭,帶來了更多咱們須要理解和記住的內容。本質上這也是一個trade-off,好用的特性須要付出代碼,易學易用的每每意味着比較死板不夠靈活。對於這個問題,咱們須要保持心態,不過好在初看時也許有些難以理解,但整體來講閉包仍是比較簡單的,我相信對大家來講必定不成問題。