閉包
網(wǎng)絡(luò)上介紹 Python 閉包的文章已經(jīng)很多了,本文將通過解決一個需求問題來了解閉包。
這個需求是這樣的,我們需要一直記錄自己的學(xué)習時間,以分鐘為單位。就好比我學(xué)習了 2 分鐘,就返回 2 ,然后隔了一陣子,我學(xué)習了 10 分鐘,那么就返回 12 ,像這樣把學(xué)習時間一直累加下去。
面對這個需求,我們一般都會創(chuàng)建一個全局變量來記錄時間,然后用一個方法來新增每次的學(xué)習時間,通常都會寫成下面這個形式:
time = 0 def insert_time(min): time = time + min return time print(insert_time(2)) print(insert_time(10))
認真想一下,會不會有什么問題呢?
其實,這個在 Python 里面是會報錯的。會報如下錯誤:
UnboundLocalError: local variable 'time' referenced before assignment
那是因為,在 Python 中,如果一個函數(shù)使用了和全局變量相同的名字且改變了該變量的值,那么該變量就會變成局部變量,那么就會造成在函數(shù)中我們沒有進行定義就引用了,所以會報該錯誤。
如果確實要引用全局變量,并在函數(shù)中對它進行修改,該怎么做呢?
我們可以使用 global 關(guān)鍵字,具體修改如下:
time = 0 def insert_time(min): global time time = time + min return time print(insert_time(2)) print(insert_time(10))
輸出結(jié)果如下:
2 12
可是啊,這里使用了全局變量,我們在開發(fā)中能盡量避免使用全局變量的就盡量避免使用。因為不同模塊,不同函數(shù)都可以自由的訪問全局變量,可能會造成全局變量的不可預(yù)知性。比如程序員甲修改了全局變量 time 的值,然后程序員乙同時也對 time 進行了修改,如果其中有錯誤,這種錯誤是很難發(fā)現(xiàn)和更正的。
全局變量降低了函數(shù)或模塊之間的通用性,不同的函數(shù)或模塊都要依賴于全局變量。同樣,全局變量降低了代碼的可讀性,閱讀者可能并不知道調(diào)用的某個變量是全局變量。
那有沒有更好的方法呢?
這時候我們使用閉包來解決一下,先直接看代碼:
time = 0 def study_time(time): def insert_time(min): nonlocal time time = time + min return time return insert_time f = study_time(time) print(f(2)) print(time) print(f(10)) print(time)
輸出結(jié)果如下:
2 0 12 0
這里最直接的表現(xiàn)就是全局變量 time 至此至終都沒有修改過,這里還是用了 nonlocal 關(guān)鍵字,表示在函數(shù)或其他作用域中使用外層(非全局)變量。那么上面那段代碼具體的運行流程是怎樣的。我們可以看下下圖:
這種內(nèi)部函數(shù)的局部作用域中可以訪問外部函數(shù)局部作用域中變量的行為,我們稱為: 閉包。更加直接的表達方式就是,當某個函數(shù)被當成對象返回時,夾帶了外部變量,就形成了一個閉包。k
閉包避免了使用全局變量,此外,閉包允許將函數(shù)與其所操作的某些數(shù)據(jù)(環(huán)境)關(guān)連起來。而且使用閉包,可以使代碼變得更加的優(yōu)雅。而且下一篇講到的裝飾器,也是基于閉包實現(xiàn)的。
到這里,就會有一個問題了,你說它是閉包就是閉包了?有沒有什么辦法來驗證一下這個函數(shù)就是閉包呢?
有的,所有函數(shù)都有一個 __closure__ 屬性,如果函數(shù)是閉包的話,那么它返回的是一個由 cell 組成的元組對象。cell 對象的 cell_contents 屬性就是存儲在閉包中的變量。
我們打印出來體驗一下:
time = 0 def study_time(time): def insert_time(min): nonlocal time time = time + min return time return insert_time f = study_time(time) print(f.__closure__) print(f(2)) print(time) print(f.__closure__[0].cell_contents) print(f(10)) print(time) print(f.__closure__[0].cell_contents)
打印的結(jié)果為:
(<cell at 0x0000000000410C48: int object at 0x000000001D6AB420>,) 2 0 2 12 0 12
從打印結(jié)果可見,傳進來的值一直存儲在閉包的 cell_contents 中,因此,這也就是閉包的最大特點,可以將父函數(shù)的變量與其內(nèi)部定義的函數(shù)綁定。就算生成閉包的父函數(shù)已經(jīng)釋放了,閉包仍然存在。
閉包的過程其實好比類(父函數(shù))生成實例(閉包),不同的是父函數(shù)只在調(diào)用時執(zhí)行,執(zhí)行完畢后其環(huán)境就會釋放,而類則在文件執(zhí)行時創(chuàng)建,一般程序執(zhí)行完畢后作用域才釋放,因此對一些需要重用的功能且不足以定義為類的行為,使用閉包會比使用類占用更少的資源,且更輕巧靈活。