Python 是一種對新手很友好的語言。但是,它也有很多較難掌握的高級功能,比如裝飾器(decorator)。很多初學者一直不理解裝飾器及其工作原理,在這篇文章中,我們將介紹裝飾器的來龍去脈。
在 Python 中,函數(shù)是一種非常靈活的結(jié)構(gòu),我們可以把它賦值給變量、當作參數(shù)傳遞給另一個函數(shù),或者當成某個函數(shù)的輸出。裝飾器本質(zhì)上也是一種函數(shù),它可以讓其它函數(shù)在不經(jīng)過修改的情況下增加一些功能。
這也就是「裝飾」的意義,這種「裝飾」本身代表著一種功能,如果用它修飾不同的函數(shù),那么也就是為這些函數(shù)增加這種功能。
一般而言,我們可以使用裝飾器提供的 @ 語法糖(Syntactic Sugar)來修飾其它函數(shù)或?qū)ο蟆H缦滤疚覀冇?@dec 裝飾器修飾函數(shù) func ():
?
@dec def?func(): ??pass
?
理解裝飾器的最好方式是了解裝飾器解決什么問題,本文將從具體問題出發(fā)一步步引出裝飾器,并展示它的優(yōu)雅與強大。
設(shè)置問題
為了解裝飾器的目的,接下來我們來看一個簡單的示例。假如你有一個簡單的加法函數(shù) dec.py,第二個參數(shù)的默認值為 10:
?
#?dec.py def?add(x,?y=10): ??return?x?+?y
?
我們來更認真地看一下這個加法函數(shù):
?
>>>?add(10,?20) 30 >>>?add>>>?add.__name__ 'add' >>>?add.__module__ '__main__' >>>?add.__defaults__?#?default?value?of?the?`add`?function (10,) >>>?add.__code__.co_varnames?#?the?variable?names?of?the?`add`?function ('x',?'y')
?
我們無需理解這些都是什么,只需要記住 Python 中的每個函數(shù)都是對象,它們有各種屬性和方法。你還可以通過 inspect 模塊查看 add() 函數(shù)的源代碼:
?
>>>?from?inspect?import?getsource >>>?print(getsource(add)) def?add(x,?y=10): ??return?x?+?y
?
現(xiàn)在你以某種方式使用該加法函數(shù),比如你使用一些操作來測試該函數(shù):
?
#?dec.py from?time?import?time def?add(x,?y=10): ??return?x?+?y print('add(10)',?????????add(10)) print('add(20,?30)',?????add(20,?30)) print('add("a",?"b")',???add("a",?"b")) Output:?i add(10)?20 add(20,?30)?50 add("a",?"b")?ab
?
假如你想了解每個操作的時間,可以調(diào)用 time 模塊:
?
#?dec.py from?time?import?time def?add(x,?y=10): ??return?x?+?y before?=?time() print('add(10)',?add(10)) after?=?time() print('time?taken:?',?after?-?before) before?=?time() print('add(20,?30)',?add(20,?30)) after?=?time() print('time?taken:?',?after?-?before) before?=?time() print('add("a",?"b")',?add("a",?"b")) after?=?time() print('time?taken:?',?after?-?before) Output: add(10)?20 time?taken:??6.699562072753906e-05 add(20,?30)?50 time?taken:??6.9141387939453125e-06 add("a",?"b")?ab time?taken:??6.9141387939453125e-06
?
現(xiàn)在,你作為一個編程人員是不是有些手癢,畢竟我們不喜歡總是復制粘貼相同的代碼。現(xiàn)在的代碼可讀性不強,如果你想改變什么,你就得修改所有出現(xiàn)的地方,Python 肯定有更好的方式。
我們可以按照如下做法,直接在 add 函數(shù)中捕捉運行時間:
?
#?dec.py from?time?import?time def?add(x,?y=10): ??before?=?time() ??rv?=?x?+?y ??after?=?time() ??print('time?taken:?',?after?-?before) ??return?rv print('add(10)',?????????add(10)) print('add(20,?30)',?????add(20,?30)) print('add("a",?"b")',???add("a",?"b"))
?
這種方法肯定比前一種要好。但是如果你還有另一個函數(shù),那么這似乎就不方便了。當我們有多個函數(shù)時:
?
#?dec.py from?time?import?time def?add(x,?y=10): ??before?=?time() ??rv?=?x?+?y ??after?=?time() ??print('time?taken:?',?after?-?before) ??return?rv def?sub(x,?y=10): ??return?x?-?y print('add(10)',?add(10)) print('add(20,?30)',?add(20,?30)) print('add("a",?"b")',?add("a",?"b")) print('sub(10)',?sub(10)) print('sub(20,?30)',?sub(20,?30))
?
因為 add 和 sub 都是函數(shù),我們可以利用這一點寫一個 timer 函數(shù)。我們希望 timer 能計算一個函數(shù)的運算時間:
?
def?timer(func,?x,?y=10): ??before?=?time() ??rv?=?func(x,?y) ??after?=?time() ??print('time?taken:?',?after?-?before) ??return?rv
?
這很不錯,不過我們必須使用 timer 函數(shù)包裝不同的函數(shù),如下所示:
?
print('add(10)',?timer(add,10)))
?
現(xiàn)在默認值還是 10 嗎?未必。那么如何做得更好呢?
這里有一個主意:創(chuàng)建一個新的 timer 函數(shù),并包裝其他函數(shù),然后返回包裝后的函數(shù):
?
def?timer(func): ??def?f(x,?y=10): ????before?=?time() ????rv?=?func(x,?y) ????after?=?time() ????print('time?taken:?',?after?-?before) ????return?rv ??return?f
?
現(xiàn)在,你只需用 timer 包裝一下 add 和 sub 函數(shù) :
?
add?=?timer(add)
?
這樣就可以了!以下是完整代碼:
?
#?dec.py from?time?import?time def?timer(func): ??def?f(x,?y=10): ????before?=?time() ????rv?=?func(x,?y) ????after?=?time() ????print('time?taken:?',?after?-?before) ????return?rv ??return?f def?add(x,?y=10): ??return?x?+?y add?=?timer(add) def?sub(x,?y=10): ??return?x?-?y sub?=?timer(sub) print('add(10)',?????????add(10)) print('add(20,?30)',?????add(20,?30)) print('add("a",?"b")',???add("a",?"b")) print('sub(10)',?????????sub(10)) print('sub(20,?30)',?????sub(20,?30)) Output: time?taken:??0.0 add(10)?20 time?taken:??9.5367431640625e-07 add(20,?30)?50 time?taken:??0.0 add("a",?"b")?ab time?taken:??9.5367431640625e-07 sub(10)?0 time?taken:??9.5367431640625e-07 sub(20,?30)?-10
?
我們來總結(jié)一下這個過程:我們有一個函數(shù)(比如 add 函數(shù)),然后用一個動作(比如計時)包裝該函數(shù)。包裝的結(jié)果是一個新函數(shù),能實現(xiàn)某些新功能。
當然了,默認值還有點問題,稍后我們會解決它。
裝飾器
現(xiàn)在,上面的解決方案以及非常接近裝飾器的思想了,使用常見行為包裝某個具體的函數(shù),這種模式就是裝飾器在做的事。使用裝飾器后的代碼是:
?
def?add(x,?y=10): ??return?x?+?y add?=?timer(add) You?write: @timer def?add(x,?y=10): ??return?x?+?y
?
它們的作用是一樣的,這就是 Python 裝飾器的作用。它實現(xiàn)的作用類似于 add = timer(add),只不過裝飾器把句法放在函數(shù)上面,且句法更加簡單:@timer。
?
#?dec.py from?time?import?time def?timer(func): ??def?f(x,?y=10): ????before?=?time() ????rv?=?func(x,?y) ????after?=?time() ????print('time?taken:?',?after?-?before) ????return?rv ??return?f @timer def?add(x,?y=10): ??return?x?+?y @timer def?sub(x,?y=10): ??return?x?-?y print('add(10)',?????????add(10)) print('add(20,?30)',?????add(20,?30)) print('add("a",?"b")',???add("a",?"b")) print('sub(10)',?????????sub(10)) print('sub(20,?30)',?????sub(20,?30))
?
參數(shù)和關(guān)鍵字參數(shù)
現(xiàn)在,還有一個小問題沒有解決。在 timer 函數(shù)中,我們將參數(shù) x 和 y 寫死了,即指定 y 的默認值為 10。有一種方法可以傳輸該函數(shù)的參數(shù)和關(guān)鍵字參數(shù),即 *args 和 **kwargs。參數(shù)是函數(shù)的標準參數(shù)(在本例中 x 為參數(shù)),關(guān)鍵字參數(shù)是已具備默認值的參數(shù)(本例中是 y=10)。代碼如下:
?
#?dec.py from?time?import?time def?timer(func): ??def?f(*args,?**kwargs): ????before?=?time() ????rv?=?func(*args,?**kwargs) ????after?=?time() ????print('time?taken:?',?after?-?before) ????return?rv ??return?f @timer def?add(x,?y=10): ??return?x?+?y @timer def?sub(x,?y=10): ??return?x?-?y print('add(10)',?????????add(10)) print('add(20,?30)',?????add(20,?30)) print('add("a",?"b")',???add("a",?"b")) print('sub(10)',?????????sub(10)) print('sub(20,?30)',?????sub(20,?30))
?
現(xiàn)在,該 timer 函數(shù)可以處理任意函數(shù)、任意參數(shù)和任意默認值設(shè)置了,因為它僅僅將這些參數(shù)傳輸?shù)胶瘮?shù)中。
高階裝飾器
你們可能會疑惑:如果我們可以用一個函數(shù)包裝另一個函數(shù)來添加有用的行為,那么我們可以再進一步嗎?我們用一個函數(shù)包裝另一個函數(shù),再被另一個函數(shù)包裝嗎?
可以!事實上,函數(shù)的深度可以隨你的意。例如,你想寫一個裝飾器來執(zhí)行某個函數(shù) n 次。如下所示:
?
def?ntimes(n): ??def?inner(f): ????def?wrapper(*args,?**kwargs): ??????for?_?in?range(n): ????????rv?=?f(*args,?**kwargs) ??????return?rv ????return?wrapper ??return?inner
?
然后你可以使用上述函數(shù)包裝另一個函數(shù),例如前文中的 add 函數(shù):
?
@ntimes(3) def?add(x,?y): ??print(x?+?y) ??return?x?+?y
?
輸出的語句表明該代碼確實執(zhí)行了 3 次。
審核編輯:湯梓紅
評論
查看更多