Python 裝飾器詳解(下)
轉自:https://blog.csdn.net/qq_27825451/article/details/84627016,博主僅對其中 demo 實現中不適合python3 版本的語法進行修改,并微調了排版,本轉載博客全部例程博主均已親測可行。
Python 3.8.5
ubuntu 18.04
聲明:此文章為,python裝飾器詳解——下篇,上一篇文章中,即詳解裝飾器——中篇 ,已經詳細講解了兩大類裝飾器,即函數裝飾器、類裝飾器的應用實例,并且分析了它們在運行的過程中的本質,給出了類裝飾器的一般模板,本文將以實際例子為依托,講解剩下的兩個內容(閉包和裝飾器的嵌套),其中,閉包是重點,包括閉包的誕生背景,閉包的定義、作用、與裝飾器的關系與區別。該系列文章共分為 上、中、下 三篇。此為第三篇。
一、閉包誕生的背景——closure
1. 一個意想不到的窘境
很多的語言都存在閉包(closure),我們也常常聽起這樣的概念,但是你真的理解它了嗎?東它的本質嗎?在講閉包之前,我打算從一個簡單的情況說起。請先看一個例子:
func_list = []
for i in range(3):def myfunc(a):return i+afunc_list.append(myfunc) #定義三個函數,將三個函數存放在一個列表中for f in func_list: #調用列表中的三個函數print(f(1))
上面的運行結果是1 2 3 嗎?但是真是的運行結果確實3 3 3。這是為什么?粗略的分析,第一個函數返回的應該是0+1,第二個返回的應該是1+1 ,第三個返回的應該是 2+1 啊,那為什么會出現這樣的結果呢?從結果上分析,應該三個函數都是返回的 2+1,這是為什么呢?因為函數定義在循環內部,雖然每一次看起來好像 i 分別為 0、1、2,實際上因為函數是沒有辦法去保存這個變化的i 的,也就是說,i,是在函數外面發生變化的,函數里面的i會一直隨著i的變化而變化,直到最終這個i不變化了,那函數里面的i是多少就是多少了。總結起來就一句話:
循環體內定義的函數是無法保存循環執行過程中的不停變化的外部變量的,即普通函數無法保存運行環境!還是不理解?
再看一個簡單的例子:
a=100def myfunc(b):return a+bprint(myfunc(200))a=200
print(myfunc(200))
上面的代碼大家都懂,運行結果為300 400。我們可以發現,因為函數內部有用到外面的a,所以函數運行的結果會隨著這個a的變化而變化,直到外面的a不變了為止,否則光函數傳遞的參數是確定的還不夠,還要取決于a。我們用兩個比較通俗的層面去理解:
-
函數內部使用到了a,b,但是a卻不是函數本身具備的財產,我雖然可以使用,但是我卻不能決定它,a變化了,函數的結果就跟著變化了,直到a取最終的值,否則函數都是變化的。(你不確定,我就永遠沒有辦法確定,你雖然就在我我身邊,但是我卻不能真正掌控你,這種感覺難道不難受嗎?)
-
用書面語言說,函數沒有辦法保存它的運行環境,什么意思,在上面的兩個例子里面,函數的運行環境都是這個模塊(即py文件),也就是說,在這個運行環境里面的一切,函數都是沒有辦法做主的,函數能夠做主只有他自身的局部作用域(包括形參)。
2. 窘境的解決辦法
func_list = []
for i in range(3):def decorator(i): #定義一個外層函數,這里之所以使用decorator,是為了后面與“裝飾器進行比較def wrapper(a): #定義一個內層函數,定義為wrapper是為了后面的比較return i + areturn wrapperfunc_list.append(decorator(i)) for f in func_list:print(f(1))
運行結果為 1 2 3 。關于為什么后面再詳細講解,這里先提供一種解決思路。
二、閉包的定義及應用
1. 閉包的定義
在一些語言中,在函數中可以(嵌套)定義另一個函數時,如果內部的函數引用了外部的函數的變量,則可能產生閉包。閉包可以用來在一個函數與一組“私有”變量之間創建關聯關系。在給定函數被多次調用的過程中,這些私有變量能夠保持其持久性。—— 維基百科
2. 閉包的作用
閉包可以用來在一個函數與一組“私有”變量之間創建關聯關系。在給定函數被多次調用的過程中,這些私有變量能夠保持其持久性(保存運行環境與變量狀態)
3. 閉包的特征
上面的描述還是不夠精煉,有沒有幾個特別的特征,讓人一眼就看出來它就是閉包呢?
-
必須要有函數的嵌套。而且外層函數必須返回內層函數,但是內層函數可以不返回值,也可以返回值;外層函數給內層函數提供了一個 “包裝起來的運行環境”,在這個“包裝的”運行環境里面,內層函數可以完全自己做主。這也是稱之為閉包的原因了。
-
內層函數一定要用到外層函數中定義的變量。如果只滿足了特征(1),也不算是閉包,一定要用到外層“包裝函數”的變量,這些變量稱之為 “自由變量”。
3. 閉包的代碼解析
依然以上面的那么例子而言,我們提出了解決窘境的辦法,那我們現在來解釋這個解決辦法到底做了什么工作。
func_list = []
for i in range(3):def decorator(i): #定義一個外層函數,這里之所以使用decorator,是為了后面與“裝飾器進行比較def wrapper(a): #定義一個內層函數,定義為wrapper是為了后面的比較return i + areturn wrapperfunc_list.append(decorator(i)) for f in func_list:print(f(1))
這里列表中存出的就是三個包裝函數decorator(1)
、decorator(2)
、decorator(3)
,其實相當于三個如下的定義:
def decorator(i=1):def wrapper(a):return i+a
因為這里wrapper
的運行環境為decorator
,不再是全局的環境,所以在wrapper
的環境中,i 是固定的,不會再變化,故而當然能夠自己做主了。
三、閉包的細節
首先明確閉包的兩個核心特征:函數嵌套 和 自由變量
其次明確閉包的兩個核心功能:保存函數的運行環境狀態 和 保存閉包環境內的局部變量
1. 閉包的細節實現
看一個簡單的閉包的例子,為了與前面的系列文章(中篇)的裝飾器進行比較,這里也采用中篇中的案例,我要為一個兩數相加的運算加密:
def decorator(c): #外層函數,產生包裝環境——即閉包d=200 #c d 都是包裝環境中的局部變量——即自由變量def wrapper(a,b): #內層函數return (a+b)*c/dreturn wrapperwrapper=decorator(150)
print(wrapper(100,300))
運行結果為:300.0
-
**為什么說它保存了函數的運行環境?**這里針對函數是內層函數即
wrapper
,它的運行環境是decorator
提供的,也就是說decorator
的環境是保存的,什么意思呢,其實就是通過一句話,wrapper=decorator(150)
也就是說,這里
wrapper
運行所依賴的c
就固定是150了,d
就固定是200了,不會再改變,無論我再給wrapper
傳遞什么參數,c
和d
是不會在變化的。當然如果我重新再執行一次wrapper=decorator(250)
,相當于是又創建了一個新的包裝環境,這里的c
就是250了。 -
**為什么說它能夠保存閉包函數內的局部變量?**眾所周知,函數的局部變量會隨著函數的調用結束而銷毀,那么為什么局部變量能夠保存呢?這里所說的局部變量指的是閉包函數的局部變量,即上面的
c
和d
。也就是說,我這里的c
和d
是保存著的,即使我已經執行wrapper(100,300)
執行完畢。
2. 自由變量的查看
我們說閉包函數的局部變量是保存著的,那如何查看呢?我們可以通過內層函數的一個屬性__closure__
查看。
print(wrapper.__closure__)
print(wrapper.__closure__[0].cell_contents)
print(wrapper.__closure__[1].cell_contents)
結果如下:
(<cell at 0x7f2c886802b0: int object at 0x5558ee7e6fe0>, <cell at 0x7f2c88680370: int object at 0x5558ee7e7620>)
150
200
可以看到 __closure__
屬性返回一個元組,而150和200則分別對應自由變量 c
和 d
。
總結:內層函數的__closure__
屬性返回一個元組;通過 wrapper.__closure__[i].cell_contents
查看第幾個自由變量的值
注意:如果閉包函數沒有返回wrapper
,即外層函數沒有返回內層函數,此時內層函數是沒有__closure__
屬性的。
總結:現在可以體會為什么說閉包保存局部變量了吧,這里的c d 作為局部變量,在函數調用結束后還能夠查看到它的值,這還不是保存,那什么是保存呢?
3. 閉包的一般模板
def decorator(*arg,**kargs): #外層函數,產生包裝環境——即閉包#自由變量區域 # 包含形參,都是包裝環境中的局部變量——即自由變量def wrapper(a,b): #內層函數return (a+b)*c/dreturn wrapperwrapper=decorator(150) #創建唯一的閉包環境
wrapper(100,300) #內層函數的調用
四、閉包與裝飾器的比較
1. 相同點
-
都是函數的嵌套,分為外層函數和內層函數,而且外層函數要返回內層函數
-
代碼的實現邏輯大同小異
-
二者都可以實現增加額外功能的目的——比如上面的“加法加密運算”
2. 不同點
-
外層函數不同,裝飾器的外層函數稱之為decorator,閉包的外層函數稱之為閉包函數closure
-
外層函數的目的不同,裝飾器的外層函數主要是提供函數形參function,閉包的形參主要目的是提供自由變量。
-
二者的特征不一樣。裝飾器的外層函數可以不提供自由變量,但是閉包的的外層函數一定要提供自由變量,因為如果不提供自由變量,必報的存在就毫無意義了,即內層函數所依賴的變量卻在閉包中根本沒有,那還要閉包干什么?
-
二者的主要目的不同。裝飾器的目的:代碼重用+額外功能。閉包的主要目的:保存函數的運行環境+保存閉包的局部變量。雖然二者可以有一些交集。
-
閉包和裝飾器本質上還是不一樣的,但是從形式上來說,大致可以認為閉包是裝飾器的子集。記住:僅僅是從形式上哦!
3. 如何理解“閉包”與“裝飾器”的本質不一樣,但是形式類似?
關于形式類似,這里就不說了,參見前面的兩篇文章和這篇文章里面的模板即可,發現他們長得很像。
為什么說本質不一樣?
-
因為對與裝飾器而言,我必須要給外層函數
decorator
傳遞一個基本參數function
,只有這樣,我才可以寫成function=decorator(function)
或者是@decorator
的形式,如果沒有這個參數,會顯示以下錯誤:decorator() takes 0 positional arguments but 1 was given
即
decorator
我必須要定義一個function
參數,否則就會顯示定義沒有參數,但給了它一個參數這種錯誤,因為function=decorator(function)
或者是@decorator
這就相當于給了他一個參數。不僅如此,裝飾器會改變函數
function
本身的__name__
屬性,參見前文。 -
但是對于閉包,外層函數就沒有這些要求,也不是一定要定義一個
function
參數,甚至我也可以不定義參數。至于兩者的本質區別,學懂了的小伙伴應該可以自己好好體會了。
五、裝飾器的嵌套
關于裝飾器的多層嵌套,理解起來相對于比較復雜,本文先做一個預告,將在系列文章的下一篇,也就是第四篇進行深入詳解,有需要的可以關注一下。
傳送門:https://blog.csdn.net/qq_27825451/article/details/102457152。