Python裝飾器(decorator)是在程序開發中經常使用到的功能,合理使用裝飾器,能讓我們的程序如虎添翼。
裝飾器的引入
初期及問題的誕生
假如現在在一個公司,有A B C三個業務部門,還有S一個基礎服務部門,目前呢,S部門提供了兩個函數,供其他部門調用,函數如下:
def f1(): print('f1 called') def f2(): print('f2 called')
在初期,其他部門這樣調用是沒有問題的,隨着公司業務的發展,現在S部門需要對函數調用假如權限驗證,如果有權限的話,才能進行調用,否則調用失敗。考慮一下,如果是我們,該怎么做呢?
方案集合
1、讓調用方也就是ABC部門在調用的時候,先主動進行權限驗證
2、S部門在對外提供的函數中,首先進行權限認證,然后再進行真正的函數操作
問題
方案一,將本不該暴露給外層的權限認證,暴露在使用方面前,同時如果有多個部門呢,要每個部門每個人都要周知到,你還不缺定別人一定會這么做,不靠譜。。。
方案二,看似看行,可是當S部門對外提供更多的需要進行權限驗證方法時,每個函數都要調用權限驗證,同樣也實在費勁,不利於代碼的維護性和擴展性
那么,有沒有一種方法能夠遵循代碼的開放閉合原則,來完美的解決此問題呢?
裝飾器引入
答案肯定是有的,不然真的是弱爆了。先看代碼
def w1(func): def inner(): print('...驗證權限...') func() return inner @w1 def f1(): print('f1 called') @w1 def f2(): print('f2 called') f1() f2()
輸出結果為
...驗證權限...
f1 called
...驗證權限...
f2 called
可以通過代碼及輸出看到,在調用f1 f2 函數時,成功進行了權限驗證,那么是怎么做到的呢?其實這里就使用到了裝飾器,通過定義一個閉包函數w1,在我們調用函數上通過關鍵詞@w1,這樣就對f1 f2函數完成了裝飾。
裝飾器原理
首先,開看我們的裝飾器函數w1,該函數接收一個參數func,其實就是接收一個方法名,w1內部又定義一個函數inner,在inner函數中增加權限校驗,並在驗證完權限后調用傳進來的參數func,同時w1的返回值為內部函數inner,其實就是一個閉包函數。
然后,再來看一下,在f1上增加@w1,那這是什么意思呢?當python解釋器執行到這句話的時候,會去調用w1函數,同時將被裝飾的函數名作為參數傳入(此時為f1),根據閉包一文分析,在執行w1函數的時候,此時直接把inner函數返回了,同時把它賦值給f1,此時的f1已經不是未加裝飾時的f1了,而是指向了w1.inner函數地址。相當於f1=w1(f1)
接下來,在調用f1()的時候,其實調用的是w1.inner函數,那么此時就會先執行權限驗證,然后再調用原來的f1(),該處的f1就是通過裝飾傳進來的參數f1。
這樣下來,就完成了對f1的裝飾,實現了權限驗證。
裝飾器知識點
執行時機
了解了裝飾器的原理后,那么它的執行時機是什么樣呢,接下來就來看一下。
國際慣例,先上代碼
def w1(fun): print('...裝飾器開始裝飾...') def inner(): print('...驗證權限...') fun() return inner @w1 def test(): print('test') test()
輸出結果為
...裝飾器開始裝飾...
...驗證權限...
test
由此可以發現,當python解釋器執行到@w1時,就開始進行裝飾了,相當於執行了如下代碼:
test = w1(test)
兩個裝飾器執行流程和裝飾結果
當有兩個或兩個以上裝飾器裝飾一個函數時,那么執行流程和裝飾結果是什么樣的呢?同樣,還是以代碼來說明問題。
def makeBold(fun): print('----a----') def inner(): print('----1----') return '<b>' + fun() + '</b>' return inner def makeItalic(fun): print('----b----') def inner(): print('----2----') return '<i>' + fun() + '</i>' return inner @makeBold @makeItalic def test(): print('----c----') print('----3----') return 'hello python decorator' ret = test() print(ret)
輸出結果:
----b---- ----a---- ----1---- ----2---- ----c---- ----3---- <b><i>hello python decorator</i></b>
可以發現,先用第二個裝飾器(makeItalic)進行裝飾,接着再用第一個裝飾器(makeBold)進行裝飾,而在調用過程中,先執行第一個裝飾器(makeBold),接着再執行第二個裝飾器(makeItalic)。
為什么呢,分兩步來分析一下。
1、裝飾時機 通過上面裝飾時機的介紹,我們可以知道,在執行到@makeBold的時候,需要對下面的函數進行裝飾,此時解釋器繼續往下走,發現並不是一個函數名,而又是一個裝飾器,這時候,@makeBold裝飾器暫停執行,而接着執行接下來的裝飾器@makeItalic,接着把test函數名傳入裝飾器函數,從而打印’b’,在makeItalic裝飾完后,此時的test指向makeItalic的inner函數地址,這時候有返回來執行@makeBold,接着把新test傳入makeBold裝飾器函數中,因此打印了’a’。
2、在調用test函數的時候,根據上述分析,此時test指向makeBold.inner函數,因此會先打印‘1‘,接下來,在調用fun()的時候,其實是調用的makeItalic.inner()函數,所以打印‘2‘,在makeItalic.inner中,調用的fun其實才是我們最原聲的test函數,所以打印原test函數中的‘c‘,‘3‘,所以在一層層調完之后,打印的結果為<b><i>hello python decorator</i></b> 。
對無參函數進行裝飾
上面例子中的f1 f2都是對無參函數的裝飾,不再單獨舉例
對有參數函數進行裝飾
在使用中,有的函數可能會帶有參數,那么這種如何處理呢?
代碼優先:
def w_say(fun): """ 如果原函數有參數,那閉包函數必須保持參數個數一致,並且將參數傳遞給原方法 """ def inner(name): """ 如果被裝飾的函數有行參,那么閉包函數必須有參數 :param name: :return: """ print('say inner called') fun(name) return inner @w_say def hello(name): print('hello ' + name) hello('wangcai')
輸出結果為:
say inner called
hello wangcai
具體說明代碼注釋已經有了,就不再單獨說明了。
此時,也許你就會問了,那是一個參數的,如果多個或者不定長參數呢,該如何處理呢?看看下面的代碼你就秒懂了。
def w_add(func): def inner(*args, **kwargs): print('add inner called') func(*args, **kwargs) return inner @w_add def add(a, b): print('%d + %d = %d' % (a, b, a + b)) @w_add def add2(a, b, c): print('%d + %d + %d = %d' % (a, b, c, a + b + c)) add(2, 4) add2(2, 4, 6)
輸出結果為:
add inner called 2 + 4 = 6 add inner called 2 + 4 + 6 = 12
利用python的可變參數輕松實現裝飾帶參數的函數。
對帶有返回值的函數進行裝飾
下面對有返回值的函數進行裝飾,按照之前的寫法,代碼是這樣的
def w_test(func): def inner(): print('w_test inner called start') func() print('w_test inner called end') return inner @w_test def test(): print('this is test fun') return 'hello' ret = test() print('ret value is %s' % ret)
輸出結果為:
w_test inner called start this is test fun w_test inner called end ret value is None
可以發現,此時,並沒有輸出test函數的‘hello’,而是None,那是為什么呢,可以發現,在inner函數中對test進行了調用,但是沒有接受不了返回值,也沒有進行返回,那么默認就是None了,知道了原因,那么來修改一下代碼:
def w_test(func): def inner(): print('w_test inner called start') str = func() print('w_test inner called end') return str return inner @w_test def test(): print('this is test fun') return 'hello' ret = test() print('ret value is %s' % ret)
輸出結果:
w_test inner called start this is test fun w_test inner called end ret value is hello
這樣就達到預期,完成對帶返回值參數的函數進行裝飾。
帶參數的裝飾器
介紹了對帶參數的函數和有返回值的函數進行裝飾,那么有沒有帶參數的裝飾器呢,如果有的話,又有什么用呢?
答案肯定是有的,接下來通過代碼來看一下吧。
def func_args(pre='xiaoqiang'): def w_test_log(func): def inner(): print('...記錄日志...visitor is %s' % pre) func() return inner return w_test_log # 帶有參數的裝飾器能夠起到在運行時,有不同的功能 # 先執行func_args('wangcai'),返回w_test_log函數的引用 # @w_test_log # 使用@w_test_log對test_log進行裝飾 @func_args('wangcai') def test_log(): print('this is test log') test_log()
輸出結果為:
...記錄日志...visitor is wangcai this is test log
簡單理解,帶參數的裝飾器就是在原閉包的基礎上又加了一層閉包,通過外層函數func_args的返回值w_test_log就看出來了,具體執行流程在注釋里已經說明了。
好處就是可以在運行時,針對不同的參數做不同的應用功能處理。
通用裝飾器
介紹了這么多,在實際應用中,如果針對沒個類別的函數都要寫一個裝飾器的話,估計就累死了,那么有沒有通用萬能裝飾器呢,答案肯定是有的,廢話不多說,直接上代碼。
def w_test(func): def inner(*args, **kwargs): ret = func(*args, **kwargs) return ret return inner @w_test def test(): print('test called') @w_test def test1(): print('test1 called') return 'python' @w_test def test2(a): print('test2 called and value is %d ' % a) test() test1() test2(9)
輸出結果為:
test called test1 called test2 called and value is 9
把上面幾種示例結合起來,就完成了通用裝飾器的功能,原理都同上,就不過多廢話了。
類裝飾器
裝飾器函數其實是一個接口約束,它必須接受一個callable對象作為參數,然后返回一個callable對象。
在python中,一般callable對象都是函數,但是也有例外。比如只要某個對象重寫了call方法,那么這個對象就是callable的。
當創建一個對象后,直接去執行這個對象,那么是會拋出異常的,因為他不是callable,無法直接執行,但進行修改后,就可以直接執行調用了,如下
class Test(object): def __call__(self, *args, **kwargs): print('call called') t = Test() print(t())
輸出為:
call called
下面,引入正題,看一下如何用類裝飾函數。
class Test(object): def __init__(self, func): print('test init') print('func name is %s ' % func.__name__) self.__func = func def __call__(self, *args, **kwargs): print('裝飾器中的功能') self.__func() @Test def test(): print('this is test func') test()
輸出結果為:
test init func name is test 裝飾器中的功能 this is test func
和之前的原理一樣,當python解釋器執行到到@Test時,會把當前test函數作為參數傳入Test對象,調用init方法,同時將test函數指向創建的Test對象,那么在接下來執行test()的時候,其實就是直接對創建的對象進行調用,執行其call方法。
預備知識
在了解wraps修飾器之前,我們首先要了解partial和update_wrapper這兩個函數,因為在wraps的代碼中,用到了這兩個函數。
partial
首先說partial函數,在官方文檔的描述中,這個函數的聲明如下:functools.partial(func, *args, **keywords)。它的作用就是返回一個partial對象,當這個partial對象被調用的時候,就像通過func(*args, **kwargs)的形式來調用func函數一樣。如果有額外的 位置參數(args) 或者 關鍵字參數(*kwargs) 被傳給了這個partial對象,那它們也都會被傳遞給func函數,如果一個參數被多次傳入,那么后面的值會覆蓋前面的值。
個人感覺這個函數很像C++中的bind函數,都是把某個函數的某個參數固定,從而構造出一個新的函數來。比如下面這個例子:
from functools import partial def add(x, y): return x+y # 這里創造了一個新的函數add2,只接受一個整型參數,然后將這個參數統一加上2 add2 = partial(add, y=2) add2(3) # 這里將會輸出5
這個函數是使用C而不是Python實現的,但是官方文檔中給出了Python實現的代碼,如下所示,大家可以進行參考:
def partial(func, *args, **keywords): def newfunc(*fargs, **fkeywords): newkeywords = keywords.copy() newkeywords.update(fkeywords) return func(*args, *fargs, **newkeywords) newfunc.func = func newfunc.args = args newfunc.keywords = keywords return newfunc
update_wrapper
接下來,我們再來聊一聊update_wrapper這個函數,顧名思義,這個函數就是用來更新修飾器函數的,具體更新些什么呢,我們可以直接把它的源碼搬過來看一下:
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__', '__annotations__') WRAPPER_UPDATES = ('__dict__',) def update_wrapper(wrapper, wrapped, assigned = WRAPPER_ASSIGNMENTS, updated = WRAPPER_UPDATES): for attr in assigned: try: value = getattr(wrapped, attr) except AttributeError: pass else: setattr(wrapper, attr, value) for attr in updated: getattr(wrapper, attr).update(getattr(wrapped, attr, {})) wrapper.__wrapped__ = wrapped return wrapper
大家可以發現,這個函數的作用就是從 被修飾的函數(wrapped) 中取出一些屬性值來,賦值給 修飾器函數(wrapper) 。為什么要這么做呢,我們看下面這個例子。
自定義修飾器v1
首先我們寫個自定義的修飾器,沒有任何的功能,僅有文檔字符串,如下所示:
def wrapper(f): def wrapper_function(*args, **kwargs): """這個是修飾函數""" return f(*args, **kwargs) return wrapper_function @wrapper def wrapped(): """這個是被修飾的函數""" print('wrapped') print(wrapped.__doc__) # 輸出`這個是修飾函數` print(wrapped.__name__) # 輸出`wrapper_function`
從上面的例子我們可以看到,我想要獲取wrapped這個被修飾函數的文檔字符串,但是卻獲取成了wrapper_function的文檔字符串,wrapped函數的名字也變成了wrapper_function函數的名字。這是因為給wrapped添加上@wrapper修飾器相當於執行了一句wrapped = wrapper(wrapped),執行完這條語句之后,wrapped函數就變成了wrapper_function函數。遇到這種情況該怎么辦呢,首先我們可以手動地在wrapper函數中更改wrapper_function的__doc__和__name__屬性,但聰明的你肯定也想到了,我們可以直接用update_wrapper函數來實現這個功能。
自定義修飾器v2
我們對上面定義的修飾器稍作修改,添加了一句update_wrapper(wrapper_function, f)。
from functools import update_wrapper def wrapper(f): def wrapper_function(*args, **kwargs): """這個是修飾函數""" return f(*args, **kwargs) update_wrapper(wrapper_function, f) # << 添加了這條語句 return wrapper_function @wrapper def wrapped(): """這個是被修飾的函數""" print('wrapped') print(wrapped.__doc__) # 輸出`這個是被修飾的函數` print(wrapped.__name__) # 輸出`wrapped`
此時我們可以發現,__doc__和__name__屬性已經能夠按我們預想的那樣顯示了,除此之外,update_wrapper函數也對__module__和__dict__等屬性進行了更改和更新。
wraps修飾器
OK,至此,我們已經了解了partial和update_wrapper這兩個函數的功能,接下來我們翻出wraps修飾器的源碼:
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__', '__annotations__') WRAPPER_UPDATES = ('__dict__',) def wraps(wrapped, assigned = WRAPPER_ASSIGNMENTS, updated = WRAPPER_UPDATES): return partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)
沒錯,就是這么的簡單,只有這么一句,我們可以看出,wraps函數其實就是一個修飾器版的update_wrapper函數,它的功能和update_wrapper是一模一樣的。我們可以修改我們上面的自定義修飾器的例子,做出一個更方便閱讀的版本。
自定義修飾器v3
from functools import wraps def wrapper(f): @wraps(f) def wrapper_function(*args, **kwargs): """這個是修飾函數""" return f(*args, **kwargs) return wrapper_function @wrapper def wrapped(): """這個是被修飾的函數 """ print('wrapped') print(wrapped.__doc__) # 輸出`這個是被修飾的函數` print(wrapped.__name__) # 輸出`wrapped`
至此,我想大家應該明白wraps這個修飾器的作用了吧,就是將 被修飾的函數(wrapped) 的一些屬性值賦值給 修飾器函數(wrapper) ,最終讓屬性的顯示更符合我們的直覺。
參考鏈接
https://segmentfault.com/a/1190000009398663
https://blog.csdn.net/u010358168/article/details/77773199
