1 引言
裝飾器(Decorators)可能是Python中最難掌握的概念之一了,也是最具Pythonic特色的技巧,深入理解並應用裝飾器,你會更加感慨——人生苦短,我用Python。
2 初步理解裝飾器
2.1 什么是裝飾器
在解釋什么是裝飾器之前,我們有必要回顧一下Python中的一些思想和概念。我們都知道,Python是一門面向對象的語言,Python基本思想就是一些皆對象,數據類型是對象、類是對象、類實例也是對象……對於接下來我們要說的裝飾器而言,最重要的是,函數也是對象!
你沒看錯,函數也和數據類型等概念一樣,都是對象,那么既然數據類型可以進行賦值操作,那么函數是不是也可以賦值呢?當然可以!
def do_something(): print('完成一些功能') if __name__ == '__main__': do = do_something do()
輸出:
完成一些功能
看,原本我們定義的函數名是do_something,但我們把函數賦值給do后,也可以通過do()調用函數。不僅如此,函數當做參數傳遞給其他函數:
def do_something(): print('正在完成功能') def func(f): f() if __name__ == '__main__': func(do_something)
輸出:
完成一些功能
正是因為Python中函數既可以賦值給其他變量名,也可以當做參數參數進其他函數,所以,上面的代碼沒有任何問題。
當然,畢竟被稱呼為函數,有別有變量之類的概念,所以它也有自己的特性,例如在函數內部,還可以定義函數,甚至作為返回值返回:
def func(): def inner_func(): print('我是內部函數') return inner_func if __name__ == '__main__': fun1 = func() fun1()
輸出結果:
我是內部函數
我們來總結一下函數的這幾個特性:
-
可以賦值給其他變量;
-
可以作為參數傳遞給其他函數;
-
可以在內部定義一個函數;
-
可以當做返回值返回。
不要疑惑我為什么要着重說明Python中函數的這幾個特性,因為裝飾器中正是這幾個特性為基石。為什么這么說呢?從本質上來說,裝飾器就是一個函數,它也具有我們上面說到的4個特性,而且充分利用了這4個特性。裝飾器接受一個普通函數作為參數,並在內部定義了一個函數,在這個內部函數中實現了一些功能,並調用了傳遞進來的函數,最后將內部函數作為返回值返回。如果一個函數把這個步驟全走了一遍,我們就可以認為,這個函數是一個裝飾器函數,或者說裝飾器。
我們來動手寫一個裝飾器:
def func(f): def inner_func(): print('{}函數開始運行……'.format(f.__name__)) f() print('{}函數結束運行……'.format(f.__name__)) return inner_func def do_something(): print('正在完成功能') if __name__ == '__main__': do_something = func(do_something) do_something()
輸出結果:
do_something函數開始運行……
正在完成功能
do_something函數結束運行……
在上面代碼中,我們將do_something方法作為參數傳遞給func方法,在func方法內部我們定義了一個inner_func方法,並在這個方法中添加了函數開始執行和結束執行的提示,在func方法最后,我們將inner_func作為參數返回,更加值得一說的是,我們重新將func函數的返回值賦給了do_something,所以,最后一行我們再次調用的do_something方法已經不再是最初的do_something函數,而是func方法內定義的inner_func函數,所以最后執行do_something函數時,會有函數開始執行和結束執行的提示功能,而后面再調用do_something函數時,也都直接使用do_something()。
正如你所預料,func函數就是一個裝飾器,捋一捋你會發現,func函數把我們上面說過的所有特性、步驟全實現了。
如果你在別處見過Python裝飾器的使用,你可能會疑惑,我們實現的func裝飾器跟你見過的裝飾器不都一樣,因為在實際應用中,裝飾器大多是與“@”符號結合起來使用。其實“@”符號所實現的功能就是 do_something = func(do_something)這行代碼的功能。來,我們嘗試一下使用“@”:
def func(f): def inner_func(): print('{}函數開始運行……'.format(f.__name__)) f() print('{}函數結束運行……'.format(f.__name__)) return inner_func @func def do_something(): print('正在完成功能') if __name__ == '__main__': do_something()
輸出結果:
do_something函數開始運行……
正在完成功能
do_something函數結束運行……
之前我們知道,func函數就是一個裝飾器,所以使用“@”符號時,我們只需要在被裝飾的函數前面加上“@func”就表示該函數被func裝飾器裝飾,在需要處直接調用do_something函數即可。
2.2 為什么要用裝飾器
在上面代碼中,我們寫了一個裝飾器func,在這個裝飾器中,使用裝飾器的好處就已經初見端倪了。
(1)可以在不對被裝飾函數做任何修改的前提下,給被裝飾函數附加上一些功能。使用@func對do_something函數進行裝飾時,我們沒有對do_something函數的代碼做什么的改變,但是被裝飾后的do_something函數卻多了開始運行和結束運行的功能。
(2)不改變被裝飾函數的調用方式。在被裝飾前,我們通過do_something()調用這個函數,被裝飾后,還是通過do_something()調用這個函數。
(3)代碼更加精簡。在上面代碼中,我們只是用@func裝飾了do_something一個函數,但是如果有多個函數需要添加開始運行和結束運行的提示功能,如果不用裝飾器,那么就需要對每一個函數進行修改,則工作量和需要修改的代碼量……用了裝飾器之后,只需要在需要添加這一功能的函數前面添加@func就可以了。
一言以蓋之,裝飾器可以在不改變原函數調用方式和代碼情況下,為函數添加一些功能,使代碼更加精簡。
我們在寫一個裝飾器來加深一下理解。相比大家都寫過代碼來統計一個函數的運行時間的功能,我們使用裝飾器來實現一下這個功能:
import time def timmer(f): def inner_func(): start_time = time.time() f() end_time = time.time() print('{}函數運行消耗時間為:{}'.format(f.__name__, end_time-start_time)) return inner_func @timmer def do_something(): print('do_something函數運行……') time.sleep(1) if __name__ == '__main__': do_something()
輸出結果:
do_something函數運行……
do_something函數運行消耗時間為:1.000662088394165
在上面例子中,我們首先定義了一個計時裝飾器timmer,當需要統計某個函數運行時間時,只需要在函數定義時,在前面添加一行寫上@timmer即可,例如上面對do_something函數運行時間進行統計,對do_something原來要實現什么功能就繼續實現這一功能,原來代碼該怎樣還怎樣,該怎么調用還怎么調用。所以說,使用裝飾器可以在不改變原函數代碼和調用方式的情況下附加上其他功能。
如果你閱讀到了這里,我想你對裝飾器已經有了初步的理解。接下來,我們繼續聊一聊更加復雜的裝飾器。
3 深入理解裝飾器
3.1 被裝飾的函數帶返回值
我們上面寫的兩個裝飾器所裝飾的do_something函數是沒有返回值的,但大多數函數可都是有返回值的。針對有返回值的函數,裝飾器該怎么寫呢?
def func(f): def inner_func(): print('{}函數開始運行……'.format(f.__name__)) ret = f() print('{}函數結束運行……'.format(f.__name__)) return ret # 這里返回值 return inner_func @func def do_something(): print('正在完成功能') return '我是返回值' if __name__ == '__main__': ret = do_something() print(ret)
輸出結果:
do_something函數開始運行……
正在完成功能
do_something函數結束運行……
我是返回值
我們知道,被裝飾后的do_something函數其實不再是最初的do_something函數,而是裝飾器內部定義的inner_func函數,所以,被裝飾的函數的返回值只需要通過裝飾器內部定義的inner_func函數返回返回即可即可。有點繞,不過對着上面的代碼應該好理解。
3.2 被裝飾函數帶參數
對於裝飾器,我們要深刻理解一件事:以上面的裝飾器func和被裝飾函數do_something為例,被裝飾后的do_something函數已經不再是原來的do_something函數,而是裝飾器內部的inner_func函數。這句話我已經在上文中我已經不止提過一次,因為真的很重要。如果被裝飾的函數有參數(加入參數為name),我們還是通過do_something(name)的方式傳遞傳輸,不過,既然我們最終調用的時候,通過do_something實質調用的inner_func函數,那么在定義裝飾器是,定義的inner_func函數時也需要接受參數。
def func(f): def inner_func(name): print('{}函數開始運行……'.format(f.__name__)) ret = f(name) print('{}函數結束運行……'.format(f.__name__)) return ret return inner_func @func def do_something(name): print('你好,{}!'.format(name)) return '我是返回值' if __name__ == '__main__': ret = do_something('姚明') print(ret)
輸出結果:
do_something函數開始運行……
你好,姚明!
do_something函數結束運行……
我是返回值
一個裝飾器可用於裝飾千千萬萬個函數,則千千萬萬個函數參數情況可能各不相同,有的沒有參數,有的可能多個參數,甚至還有關鍵字參數,對於這參數情況不同的函數,我們不可能為每個函數都寫一個func裝飾器,那怎么辦呢?
Python中提供了*args, **kwargs這種機制來接受任意位置的位置參數和關鍵字參數,參數前面帶*表示接受任意個數位置參數,接收到的所有位置參數存儲在變量名為args的元組中,帶**表示接受任意個數的關鍵字參數,接收到的所有關鍵字參數以字典的形式參數在變量名為kwargs的字典中。
當我們知道只有位置參數,但不知道有多少個位置參數是func裝飾器可以這么寫:
def func(f): def inner_func(*name): print('{}函數開始運行……'.format(f.__name__)) ret = f(*name) print('{}函數結束運行……'.format(f.__name__)) return ret return inner_func @func def do_something(name): print('你好,{}!'.format(name)) @func def do_something_2(name_1, name_2): print('你好,{}!'.format(name_1)) print('你好,{}!'.format(name_2)) @func def do_something_3(*name): for n in name: print('你好,{}!'.format(n)) if __name__ == '__main__': do_something('姚明') print('-------------------------------') do_something_2('姚大明', '姚小明') print('-------------------------------') do_something_3('姚一明', '姚二明', '姚三明', '姚四明')
輸出結果:
do_something函數開始運行……
你好,姚明!
do_something函數結束運行……
-------------------------------
do_something_2函數開始運行……
你好,姚大明!
你好,姚小明!
do_something_2函數結束運行……
-------------------------------
do_something_3函數開始運行……
你好,姚一明!
你好,姚二明!
你好,姚三明!
你好,姚四明!
do_something_3函數結束運行……
上面例子定義func裝飾器時,我們用*name來接受任意個數的位置參數,可別以為只能用*args,args只是一個變量名,只不過約定俗成,用的多一些,實際開發時你愛取啥名就用啥名,對於這個知識點不再多說,畢竟本篇主角是裝飾器。我們繼續裝飾器內容!
當我們知道只有關鍵字參數,卻不知道參數個數時,可以func裝飾器這么寫:
def func(f): def inner_func(**name): print('{}函數開始運行……'.format(f.__name__)) ret = f(**name) print('{}函數結束運行……'.format(f.__name__)) return ret return inner_func @func def do_something(name='無名氏'): print('你好,{}!'.format(name)) @func def do_something_2(name_1='無名氏', name_2='無名氏'): print('你好,{}!'.format(name_1)) print('你好,{}!'.format(name_2)) @func def do_something_3(**name): for n in name.keys(): print('你好,{}!'.format(name[n])) if __name__ == '__main__': do_something(name='姚明') print('-------------------------------') do_something_2(name_1='姚大明', name_2='姚小明') print('-------------------------------') do_something_3(name_1='姚一明', name_2='姚二明', name_3='姚三明', name_4='姚四明')
輸出結果:
do_something函數開始運行……
你好,姚明!
do_something函數結束運行……
-------------------------------
do_something_2函數開始運行……
你好,姚大明!
你好,姚小明!
do_something_2函數結束運行……
-------------------------------
do_something_3函數開始運行……
你好,姚一明!
你好,姚二明!
你好,姚三明!
你好,姚四明!
do_something_3函數結束運行……
事實上,大多數情況下,我們對被裝飾函數是一無所知的——我們不知道有多少個位置參數、多少個關鍵字參數,甚至對有沒有位置參數、關鍵字參數都不知道,這時候,我們就只能*args和**kwargs齊上陣了:
def func(f): def inner_func(*name1, **name2): print('{}函數開始運行……'.format(f.__name__)) ret = f(*name1, **name2) print('{}函數結束運行……'.format(f.__name__)) return ret return inner_func @func def do_something(name): print('你好,{}!'.format(name)) @func def do_something_2(name_1, name_2='無名氏'): print('你好,{}!'.format(name_1)) print('你好,{}!'.format(name_2)) @func def do_something_3(*name1, **name2): for n in name1: print('你好,{}!'.format(n)) for n in name2.keys(): print('你好,{}!'.format(name2[n])) if __name__ == '__main__': do_something(name='姚明') print('-------------------------------') do_something_2(name_1='姚大明', name_2='姚小明') print('-------------------------------') do_something_3('姚一明', '姚二明', '姚三明', name_4='姚四明')
輸出結果:
do_something函數開始運行……
你好,姚明!
do_something函數結束運行……
-------------------------------
do_something_2函數開始運行……
你好,姚大明!
你好,姚小明!
do_something_2函數結束運行……
-------------------------------
do_something_3函數開始運行……
你好,姚一明!
你好,姚二明!
你好,姚三明!
你好,姚四明!
do_something_3函數結束運行……
3.3 裝飾器本身帶參數
我們上面寫的裝飾器都沒有參數,或者說只有一個自帶參數,也就是被裝飾函數f。其實,裝飾器也是可以有其他參數的,這樣的裝飾器更加靈活。我們通過實例來說明:現在我們要對上面的func裝飾器進行改進,需要做到靈活控制裝飾器是用中文輸出還是用英文輸出,代碼如下。
def language(lang='中文'): # 這里帶參數 def func(f): # 往里嵌套了一層 def inner_func(*name1, **name2): if lang=='中文': print('{}函數開始運行……'.format(f.__name__)) else: print('The function of {} starts runging…'.format(f.__name__)) ret = f(*name1, **name2) if lang=='中文': print('{}函數結束運行……'.format(f.__name__)) else: print('The function of {} ends runging…'.format(f.__name__)) return ret return inner_func return func @language('中文') def do_something(name): print('你好,{}!'.format(name)) @language('English') def do_something_2(name): print('你好,{}!'.format(name)) if __name__ == '__main__': do_something(name='姚明') print('-------------------------') do_something_2(name='姚明')
輸出如下:
do_something函數開始運行……
你好,姚明!
do_something函數結束運行……
-------------------------
The function of do_something_2 starts runging…
你好,姚明!
The function of do_something_2 ends runging…
可以看到,通過裝飾器帶參數的方式,我們只需要在定義被裝飾函數時,指定裝飾器參數,就可以靈活控制每個被裝飾函數提示的語言。
當然,必須承認,裝飾器帶參數后,看起來更加復雜,需要多嵌套一層函數,由最外層的函數接受參數,里層函數才是真正的裝飾器。使用裝飾器時,會首先運行帶參數的最外層函數,返回裝飾器,這一步Python會自動幫我們完成。所以,帶參數的裝飾器甚至可以這么使用:
h = language('中文') @h def do_something(name): print('你好,{}!'.format(name))
3.4 多層裝飾器
裝飾器也是可以多層嵌套使用的,也就是說,一個函數可以通過是被多個裝飾器所裝飾,執行順序是從下到上的優先順序加載裝飾:
# -*- coding: utf-8 -*- import time print(1) def func(f): print(2) def inner_func(*name1, **name2): print('{}函數開始運行……'.format(f.__name__)) f(*name1, **name2) print('{}函數結束運行……'.format(f.__name__)) print(3) return inner_func print(4) def timmer(f): print(5) def inner_timmer(*args, **kwargs): print('開始計時……') start_time = time.time() f(*args, **kwargs) end_time = time.time() print('開始結束……') time_cost = end_time - start_time print('{}函數運行時長為:{}秒'.format(f.__name__, time_cost)) print(6) return inner_timmer print(7) @func @timmer def do_something(name): time.sleep(1) print('你好,{}!'.format(name)) print(8) def do_something_2(name): time.sleep(1) print('你好,{}!'.format(name)) if __name__ == '__main__': print(9) do_something(name='姚明') print('-------------------------') func(timmer(do_something_2))(name='姚明') # 執行效果與上面使用了@符號的do_something一樣
輸出結果:
1
4
7
5
6
2
3
8
9
inner_timmer函數開始運行……
開始計時……
你好,姚明!
開始結束……
do_something函數運行時長為:1.0004358291625977秒
inner_timmer函數結束運行……
-------------------------
5
6
2
3
inner_timmer函數開始運行……
開始計時……
你好,姚明!
開始結束……
do_something_2函數運行時長為:1.000028133392334秒
inner_timmer函數結束運行……
在上面代碼中,我們同時用func和timmer兩個裝飾器來裝飾do_something,從運行結果中可以看出,兩個裝飾器都發揮了作用。同時,為了方便大家理解,我們使用不帶@符號的來使用兩個裝飾器裝飾,兩者運行結果是一樣的,結合代碼中的輸出標記,我們來分析一下裝飾器的執行過程:開始運行后,1->4->7這幾個過程我相信大家都是可以理解的,到了位置7后,遇到了@符號標識的裝飾器,而且是多層的,兩個@裝飾器相當於func(timmer(do_something_2)),所以是先執行timmer函數獲取返回值作為參數傳遞給func,所以有了7之后是5->6,timmer函數返回值是inner_timmer函數,這時候就相當於func(inner_timmer),所以程序退出timmer函數后進入func函數,就有了2->3,從func函數返回后,繼續向下執行遇到位置8,然后就進入了主函數運行,所以是8->9,此時的函數是被裝飾過的,本質已經是func函數返回的inner_func函數了,所以最終在主函數中執行do_something時執行的是inner_func方法,所以先輸出了func裝飾器的函數開始提示,然后才是timmer裝飾器的計時開始提示。
嗯,有點復雜!
3.5 類裝飾器
通過上面的介紹,我想你已經知道,@符號是裝飾器的一個表示,或者說一個裝飾器語法糖,當使用@時,例如@A,這種語法糖會自動將被裝飾函數f作為參數傳遞給A函數,然后將A函數的返回值重新f給f這個變量名,這就是@語法糖幫我們做的事情,概括來說就是f=A(f)()。
我們現在發散一下思維,假設A如果是一個類會怎么樣呢?我們知道當A是一個類時,A()表示調用A類的構造函數__init__實例化一個A類對象,那么A(f)就表示將函數f作為參數傳遞給A類的構造方法__init__來構造一個A類實例對象,如果使用了@符號,那么這種語法機制就還會在A(f)后面加一個括號變成A(f)(),這是什么鬼?執行一個類實例對象?不過話要說回來,如果A(f)()這種結構要是沒有問題,能夠成功執行,是不是就意味着Python中類也可以成為裝飾器了呢?確實如此。我們先看看通過A()()執行一個類實例對象會怎么樣:
class A(object): def __init__(self): print('實例化一個A類對象') def __call__(self, *args, **kwargs): print('__call__方法被調用……') if __name__ == '__main__': A()()
輸出結果:
實例化一個A類對象
__call__方法被調用……
看到沒,通過A()()執行一個類實例對象時,執行的是A類內部的__call__方法。那么如果用A用作裝飾器時,@A返回的就是A類內部定義的__call__方法,相當於函數裝飾器func內的inner_func。來,我們感受一下類裝飾器:
class A(object): def __init__(self, f): print('實例化一個A類對象……') self.f = f def __call__(self, *args, **kwargs): print('{}函數開始運行……'.format(self.f.__name__)) self.f(*args, **kwargs) print('{}函數結束運行……'.format(self.f.__name__)) @A def do_something(name): print('你好,{}!'.format(name)) if __name__ == '__main__': do_something('姚明')
輸出結果:
實例化一個A類對象……
do_something函數開始運行……
你好,姚明!
do_something函數結束運行……
如果類裝飾器帶參數呢?這時候,類裝飾器的參數也可定是通過@A(t)的形式傳遞,這時候,因為@語法糖會自動加括號的原因,結構就編程這樣A(t)(),A(t)是類實例對象,A(t)()就是__call__方法,所以,@語法糖會把被裝飾函數f作為參數傳遞給__call__方法,被裝飾函數的參數需要在__call__內部定義一個函數來接受。也就是話說,定義類裝飾器時,裝飾器的參數通過__init__構造方法接收,被裝飾函數的作為參數被__call__方法接收,。(這段子很繞,有點燒腦,沒點兒Python基礎還真不好理解,語言表達能力優先感覺要枯竭了)
import time class A(object): def __init__(self, t): print('實例化一個A類對象……') self.t = t def __call__(self, f): def inner_A(*args, **kwargs): print('延遲{}秒后開始執行……'.format(self.t)) time.sleep(self.t) print('{}函數開始運行……'.format(f.__name__)) f(*args, **kwargs) print('{}函數結束運行……'.format(f.__name__)) return inner_A @A(1) def do_something(name): print('你好,{}!'.format(name)) if __name__ == '__main__': do_something('姚明')
輸出結果:
實例化一個A類對象……
延遲1秒后開始執行……
do_something函數開始運行……
你好,姚明!
do_something函數結束運行……
無論是函數裝飾器還是類裝飾器,原理上是一樣的,區別在於如果A是函數,A()是直接調用函數,而A是類時,A()是實例化,通過A()()是調用A類的__call__方法。
4 Python中內置的裝飾器
4.1 @property,@setter,@deleter
@property,@setter,@deleter這三個裝飾器提供了更加友好的方式來獲取、設置或刪除類中的屬性。@property裝飾器所裝飾的函數可以像訪問屬性一樣調用函數,注意,@property裝飾器必須先於@setter,@deleter使用,且三者說裝飾的函數必須同名。
class A(object): def __init__(self, v): print('實例化一個A類對象……') self.__value = v @property def value(self): print('取值時被調用') return self.__value @value.setter def value(self, value): print('賦值時被調用') self.__value = value @value.deleter def value(self): print('刪除值時被調用……') del self.__value if __name__ == '__main__': a = A(123) print('-------------') print('__value的值為:{}'.format(a.value)) print('-------------') a.value = 234 print('__value的值為:{}'.format(a.value)) print('--------------') del a.value print('__value的值為:{}'.format(a.value))
輸出為:
Traceback (most recent call last):
實例化一個A類對象……
-------------
取值時被調用
File "E:/WorkProjectCode/study_pymysql/study_secorators/test2.py", line 33, in <module>
__value的值為:123
-------------
print('__value的值為:{}'.format(a.value))
賦值時被調用
取值時被調用
__value的值為:234
File "E:/WorkProjectCode/study_pymysql/study_secorators/test2.py", line 11, in value
--------------
return self.__value
刪除值時被調用……
取值時被調用
AttributeError: 'A' object has no attribute '_A__value'
運行產生異常,因為最后訪問了已經刪除了的元素。
4.2 @classmethod
在一個類中,如果一個方法被@classmethod所裝飾,就代表該方法與類綁定,而不是與實例對象綁定,第一個參數cls由Python機制自動傳遞,表示類本身。
class A(object): @classmethod def f(cls): print('當前類名為:{}'.format(cls.__name__)) if __name__ == '__main__': A.f()
輸出結果:
當前類名為:A
4.3 @staticmethod
被@staticmethod所裝飾的方法為靜態方法,靜態方法一般使用場景就是和類相關的操作,但是又不會依賴和改變類、實例的狀態,比如一些工具方法。
import time class A(object): @staticmethod def f(): time_now = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())) return time_now if __name__ == '__main__': print(A.f()) print(A().f())
輸出:
2019-08-16 19:29:32
2019-08-16 19:29:32