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