Python的裝飾器(decorator)可以說是Python的一個(gè)神器,它可以在不改變一個(gè)函數(shù)代碼和調(diào)用方式的情況下給函數(shù)添加新的功能。Python的裝飾器同時(shí)也是Python學(xué)習(xí)從入門到精通過程中必需要熟練掌握的知識(shí)。小編我當(dāng)初學(xué)習(xí)Python時(shí)差點(diǎn)被裝飾器搞暈掉,今天嘗試用淺顯的語(yǔ)言解釋下Python裝飾器的工作原理及如何編寫自己的裝飾器吧。
Python裝飾器的本質(zhì)
Python的裝飾器本質(zhì)上是一個(gè)嵌套函數(shù),它接受被裝飾的函數(shù)(func)作為參數(shù),并返回一個(gè)包裝過的函數(shù)。這樣我們可以在不改變被裝飾函數(shù)的代碼的情況下給被裝飾函數(shù)或程序添加新的功能。Python的裝飾器廣泛應(yīng)用于緩存、權(quán)限校驗(yàn)(如django中的@login_required和@permission_required裝飾器)、性能測(cè)試(比如統(tǒng)計(jì)一段程序的運(yùn)行時(shí)間)和插入日志等應(yīng)用場(chǎng)景。有了裝飾器,我們就可以抽離出大量與函數(shù)功能本身無關(guān)的代碼,增加一個(gè)函數(shù)的重用性。
試想你寫了很多程序,一直運(yùn)行也沒啥問題。有一天老板突然讓你統(tǒng)計(jì)每個(gè)程序都運(yùn)行了多長(zhǎng)時(shí)間并比較下運(yùn)行效率。此時(shí)如果你去手動(dòng)修改每個(gè)程序的代碼一定會(huì)讓你抓狂,而且還破壞了那些程序的重用性。聰明的程序員是絕不能干這種蠢事的。此時(shí)你可以編寫一個(gè)@time_it的裝飾器(代碼如下所示)。如果你想打印出某個(gè)函數(shù)或程序運(yùn)行時(shí)間,只需在函數(shù)前面@一下,是不是很帥?
運(yùn)行結(jié)果如下:
Func1 is running.
用時(shí):2.0056326389312744
由于Python裝飾器的工作原理主要依賴于嵌套函數(shù)和閉包,所以我們必須先對(duì)嵌套函數(shù)和閉包有深入的了解。嵌套函數(shù)和閉包幾乎是Python工作面試必考題哦。
嵌套函數(shù)
如果在一個(gè)函數(shù)的內(nèi)部還定義了另一個(gè)函數(shù)(注意: 是定義,不是引用!),這個(gè)函數(shù)就叫嵌套函數(shù)。外部的我們叫它外函數(shù),內(nèi)部的我們叫他內(nèi)函數(shù)。
我們先來看一個(gè)最簡(jiǎn)單的嵌套函數(shù)的例子。我們?cè)趏uter函數(shù)里又定義了一個(gè)inner函數(shù),并調(diào)用了它。你注意到了嗎? 內(nèi)函數(shù)在自己作用域內(nèi)查找局部變量失敗后,會(huì)進(jìn)一步向上一層作用域里查找。
def outer():
x = 1
def inner():
y = x + 1
print(y)
inner()
outer() #輸出結(jié)果 2
如果我們?cè)谕夂瘮?shù)里不直接調(diào)用內(nèi)函數(shù),而是通過return inner返回一個(gè)內(nèi)函數(shù)的引用 這時(shí)會(huì)發(fā)生什么呢? 你將會(huì)得到一個(gè)內(nèi)函數(shù)對(duì)象,而不是運(yùn)行結(jié)果。
上述這個(gè)案例比較簡(jiǎn)單,因?yàn)閛uter和inner函數(shù)都是沒有參數(shù)的。我們現(xiàn)在對(duì)上述代碼做點(diǎn)改動(dòng),加入?yún)?shù)。你可以看到外函數(shù)的參數(shù)或變量可以很容易傳遞到內(nèi)函數(shù)。
def outer(x):
a = x
def inner(y):
b = y
print(a+b)
return inner
f1 = outer(1) # 返回inner函數(shù)對(duì)象
f1(10) # 相當(dāng)于inner(10)。輸出11
如果上例中外函數(shù)的變量x換成被裝飾函數(shù)對(duì)象(func),內(nèi)函數(shù)的變量y換成被裝飾函數(shù)的參數(shù),我們就可以得到一個(gè)通用的裝飾器啦(如下所示)。你注意到了嗎? 我們?cè)跊]對(duì)func本身做任何修改的情況下,添加了其它功能, 從而實(shí)現(xiàn)了對(duì)函數(shù)的裝飾。
請(qǐng)你仔細(xì)再讀讀上面這段代碼,我們的decorator返回的僅僅是inner函數(shù)嗎? 答案是不。它返回的其實(shí)是個(gè)閉包(Closure)。整個(gè)裝飾器的工作都依賴于Python的閉包原理。
閉包(Closure)
閉包是Python編程一個(gè)非常重要的概念。如果一個(gè)外函數(shù)中定義了一個(gè)內(nèi)函數(shù),且內(nèi)函數(shù)體內(nèi)引用到了體外的變量,這時(shí)外函數(shù)通過return返回內(nèi)函數(shù)的引用時(shí),會(huì)把定義時(shí)涉及到的外部引用變量和內(nèi)函數(shù)打包成一個(gè)整體(閉包)返回。我們?cè)诳聪轮g案例。我們的outer方法返回的只是內(nèi)函數(shù)對(duì)象嗎? 錯(cuò)。我們的outer函數(shù)返回的實(shí)際上是一個(gè)由inner函數(shù)和外部引用變量(a)組成的閉包!
def outer(x):
a = x
def inner(y):
b = y
print(a+b)
return inner
f1 = outer(1) # 返回inner函數(shù)對(duì)象+局部變量1(閉包)
f1(10) # 相當(dāng)于inner(10)。輸出11
一般一個(gè)函數(shù)運(yùn)行結(jié)束的時(shí)候,臨時(shí)變量會(huì)被銷毀。但是閉包是一個(gè)特別的情況。當(dāng)外函數(shù)發(fā)現(xiàn),自己的臨時(shí)變量會(huì)在將來的內(nèi)函數(shù)中用到,自己在結(jié)束的時(shí)候,返回內(nèi)函數(shù)的同時(shí),會(huì)把外函數(shù)的臨時(shí)變量同內(nèi)函數(shù)綁定在一起。這樣即使外函數(shù)已經(jīng)結(jié)束了,內(nèi)函數(shù)仍然能夠使用外函數(shù)的臨時(shí)變量。這就是閉包的強(qiáng)大之處。
如何編寫一個(gè)通用的裝飾器
我們現(xiàn)在可以開始動(dòng)手寫個(gè)名為hint的裝飾器了,其作用是在某個(gè)函數(shù)運(yùn)行前給我們提示。這里外函數(shù)以hint命名,內(nèi)函數(shù)以常用的wrapper(包裹函數(shù))命名。
我們現(xiàn)在對(duì)hello已經(jīng)進(jìn)行了裝飾,當(dāng)我們調(diào)用hello()時(shí),我們可以看到如下結(jié)果。
>>> hello()
hello is running.
Hello!
值得一提的是被裝飾器裝飾過的函數(shù)看上去名字沒變,其實(shí)已經(jīng)變了。當(dāng)你運(yùn)行hello()后,你會(huì)發(fā)現(xiàn)它的名字已經(jīng)悄悄變成了wrapper,這顯然不是我們想要的(如下圖所示)。這一點(diǎn)也不奇怪,因?yàn)橥夂瘮?shù)返回的是由wrapper函數(shù)和其外部引用變量組成的閉包。
為了解決這個(gè)問題保證裝飾過的函數(shù)__name__屬性不變,我們可以使用functools模塊里的wraps方法,先對(duì)func變量進(jìn)行wraps。下面這段代碼可以作為編寫一個(gè)通用裝飾器的示范代碼,注意收藏哦。
from functools import wraps
def hint(func):
@wraps(func)
def wrapper(*args, **kwargs):
print('{} is running'.format(func.__name__))
return func(*args, **kwargs)
return wrapper
@hint
def hello():
print('Hello!')
恭喜你,你已經(jīng)學(xué)會(huì)寫一個(gè)比較通用的裝飾器啦,并保證裝飾過的函數(shù)__name__屬性不變啦。當(dāng)然使用嵌套函數(shù)也有缺點(diǎn),比如不直觀。這時(shí)你可以借助Python的decorator模塊(需事先安裝)可以簡(jiǎn)化裝飾器的編寫和使用。如下所示。
編寫帶參數(shù)的高級(jí)裝飾器
前面幾個(gè)裝飾器一般是內(nèi)外兩層嵌套函數(shù)。如果我們需要編寫的裝飾器本身是帶參數(shù)的,我們需要編寫三層的嵌套函數(shù),其中最外一層用來傳遞裝飾器的參數(shù)。現(xiàn)在我們要對(duì)@hint裝飾器做點(diǎn)改進(jìn),使其能通過@hint(coder='John')傳遞參數(shù)。該裝飾器在函數(shù)運(yùn)行前給出提示的時(shí)候還顯示函數(shù)編寫人員的名字。完整代碼如下所示:
from functools import wraps
def hint(coder):
def wrapper(func):
@wraps(func)
def inner_wrapper(*args, **kwargs):
print('{} is running'.format(func.__name__))
print('Coder: {}'.format(coder))
return func(*args, **kwargs)
return inner_wrapper
return wrapper
@hint(coder='John')
def hello():
print('Hello!')
下面這段代碼是一段經(jīng)典的Python裝飾器代碼,顯示了@cache這個(gè)裝飾器怎么編寫和工作的。它需要使用緩存實(shí)例做為一個(gè)參數(shù),所以也是三層嵌套函數(shù)。
Python的裝飾器不僅可以用嵌套函數(shù)來編寫,還可以使用類來編寫。其調(diào)用__init__方法創(chuàng)建實(shí)例,傳遞參數(shù),并調(diào)用__call__方法實(shí)現(xiàn)對(duì)被裝飾函數(shù)功能的添加。
from functools import wraps
#類的裝飾器寫法, 不帶參數(shù)
class Hint(object):
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print('{} is running'.format(self.func.__name__))
return self.func(*args, **kwargs)
#類的裝飾器寫法, 帶參數(shù)
class Hint(object):
def __init__(self, coder=None):
self.coder = coder
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
print('{} is running'.format(func.__name__))
print('Coder: {}'.format(self.coder))
return func(*args, **kwargs) # 正式調(diào)用主要處理函數(shù)
return wrapper
小結(jié)
本文總結(jié)了什么是Python的裝飾器及其工作原理,并重點(diǎn)介紹了嵌套函數(shù)和閉包原理。最后詳細(xì)展示了如何編寫一個(gè)通用裝飾器及帶參數(shù)的高級(jí)裝飾器, 包括使用類來編寫裝飾器。大家要熟練掌握哦??床欢目梢韵燃尤胛⑿攀詹匾院笤俜磸?fù)閱讀。
大江狗
2018.11.29
參考資料
https://www.cnblogs.com/Lin-Yi/p/7305364.html
http://python.jobbole.com/81683/
http://lib.csdn.net/article/python/62942
http://lib.csdn.net/article/python/64769
http://www.cnblogs.com/cicaday/p/python-decorator.html
聯(lián)系客服