Python 裝飾器詳解(上)
轉自:https://blog.csdn.net/qq_27825451/article/details/84396970,博主僅對其中 demo 實現中不適合python3 版本的語法進行修改,并微調了排版,本轉載博客全部例程博主均已親測可行。
Python 3.8.5
ubuntu 18.04
一、先從一種情況看起
1. 裝飾器decorator的由來
裝飾器的定義很是抽象,我們來看一個小例子。先定義一個簡單的函數:
def myfunc():print('我是函數myfunc')myfunc() #調用函數
然后呢,我想看看這個函數執行這個函數用了多長時間,好吧,那么我們可以這樣做:
import time
def myfunc():start = time.time()print('我是函數myfunc')end = time.time()print(f'函數所花費的時間為 :{end - start}')myfunc() #函數調用
我們現在已經達到了我們的目的。但是如果是我們還想繼續給另外的一些函數也實現同樣的功能。那我們是不是給每個函數都添加這么幾句話呢?當然可以,但是不高效,而且很麻煩。如果有某一種方式可以一次性解決所有的問題,那自然最好不過了,于是“裝飾器”就應運而生。
在上面的例子中,函數本身的功能只是打印一句話而已,但是經過改造后的函數不僅要能夠打印這一句話,還要能夠顯示函數執行所花費的時間,這相當于我要給這個函數添加額外的功能,注意這個關鍵字,其實“裝飾器”就是專門給函數添加額外的功能的。
2. 添加額外功能的簡單實現——非“裝飾器”實現
還記得嗎,函數在Python中是一等公民,那么我們可以考慮重新定義一個函數timeit,將myfunc的引用傳遞給他,然后在timeit中調用myfunc并進行計時,這樣,我們就達到了不改動myfunc定義但是又添加了額外功能的目的,代碼如下:
import timedef myfunc():print("我是函數myfunc")def timeit(function):start = time.time()function()end =time.time()print(f'函數執行所花費的時間為:{end-start}')timeit(myfunc)
運行結果為:
我是函數myfunc
函數執行所花費的時間為:1.9311904907226562e-05
上面的代碼看起來邏輯上并沒有問題,也達到了我們所要實現的目的!但是,我們雖然沒有修改函數myfunc定義中的代碼,但是我們似乎修改了調用部分的代碼。原本我們是這樣調用的:myfunc()
,修改以后變成了:timeit(myfunc)
。這樣的話,如果myfunc在N處都被調用了,你就不得不去修改這N處的代碼。或者更極端的,考慮其中某處調用的代碼無法修改這個情況,比如:這個函數是你交給別人使用的。
其實將函數作為參數傳遞,已經具備了裝飾器的雛形了,但是上面的實現還不夠好,下面會給出更好地實現方式。
二、什么是裝飾器decorator
一般而言,如果我需要給函數添加額外的某一些功能,我需要修改函數的源代碼,但是如前面所說,這樣麻煩,而且不高效,裝飾器就是專門的解決方案!
1. 什么是裝飾器?——兩個層面
在Python里面有兩層定義:
第一:從設計模式的層面上
裝飾器是一個很著名的設計模式,經常被用于有切面需求的場景,較為經典的應用有插入日志、增加計時邏輯來檢測性能、加入事務處理等。裝飾器是解決這類問題的絕佳設計,有了裝飾器,我們就可以抽離出大量函數中與函數功能本身無關的雷同代碼并繼續重用。概括的講,裝飾器的作用就是為已經存在的對象添加額外的功能。
第二:從Python的語法層面上(其實第二種本質上也是第一種,只不過在語法上進行了規范化)
簡言之,python裝飾器就是用于拓展原來函數功能的一種函數,這個函數的特殊之處在于它的返回值也是一個函數,使用python裝飾器的好處就是在不用更改原函數的代碼前提下給函數增加新的功能。 如此一來,我們要想拓展原來函數代碼,就不需要再在函數里面修改源代碼了。
2. 裝飾器的作用——兩方面
(1)抽離雷同代碼,加以重用
(2)為函數添加額外的功能
3. 裝飾器的使用場景
(1)緩存裝飾器
(2)權限驗證裝飾器
(3)計時裝飾器
(4)日志裝飾器
(5)路由裝飾器
(6)異常處理裝飾器
(7)錯誤重試裝飾器
三、裝飾器的實現
1. 裝飾器的逐步實現
針對上面改進版的代碼所存在的哪些問題,我們想出了解決辦法:
既然修改N處的調用代碼很麻煩,我們就來想想辦法不修改調用代碼;如果不修改調用代碼,也就意味著調用myfunc()
需要產生調用timeit(myfunc)
的效果。
因為python中一切皆對象,故而我們可以想到將timeit
賦值給myfunc
,
代碼如下:
import timedef myfunc():print("我是函數myfunc")def timeit(function):start = time.time()function()end =time.time()print(f'函數執行所花費的時間為:{end-start}')myfunc=timeit #將timeit賦值給原來的myfunc
myfunc()
但是上面的調用并不會成功,會顯示出如下錯誤:
TypeError: timeit() missing 1 required positional argument: 'function'
這是因為將timeit
賦值給myfunc
之后,此時myfunc
和timeit
表示的是同一個東西,但是timeit
似乎帶有一個參數function
,而在調用myfunc()
的時候并沒有傳入任何參數,所以并不會成功。
但是上面的調用雖然沒有成功,卻給我們指出了一條重要的線索,因為上面的代碼已經解決“修改調用代碼”的問題,只不過是參數沒有統一而已,那就想辦法把參數統一吧!那就再添加一個函數唄!什么意思?
因為參數不統一,如果timeit()
并不是直接添加額外的功能,而是返回一個與myfunc
參數列表一致的函數。而原來timeit
需要添加額外功能的代碼再在timeit
里面定義一個函數,由它去完成不就可以了嗎,將timeit(myfunc)
的返回值賦值給myfunc
,然后,調用myfunc()
的代碼完全不用修改。——即我們依然是調用myfunc
(調用代碼沒變),但是同樣卻達到了添加額外功能的效果!
代碼如下:
import time
#原來的函數myfunc
def myfunc():print("我是函數myfunc")#定義一個計時器
def timeit(function):'''timeit函數負責返回一個wrapper,wrapper的參數要與原來的myfunc保持相同這樣一來,執行 myfunc=timeit(myfunc) myfunc完全等價于wrapperwrapper函數負責添加額外功能'''def wrapper():start = time.time()function()end =time.time()print(f'函數執行所花費的時間為:{end-start}')return wrappermyfunc=timeit(myfunc) #注意,這里與前面的 “myfunc=timeit”是有所區別的哦
myfunc() #還和原來調用myfunc()一樣,但是達到了添加額外功能的效果
執行結果:
我是函數myfunc
函數執行所花費的時間為:1.8596649169921875e-05
總結:在上面的函數定義和調用中,看起來我的調用myfunc()和原來并沒有任何不同,但是卻已經添加了額外的效果。它解決前面存在的兩個問題:
(1)不用修改函數源代碼,也不用修改調用函數的代碼,完全跟調用最原始的myfunc()代碼一樣,但是卻添加了額外功能;
(2)解決了timeit和myfunc的參數不統一問題,那就是再添加一層wrapper;
——這就是裝飾器。
上面的裝飾器就是最原始的版本,但是python中引入了專門的“語法糖”來實現裝飾器,這樣看起來更加專業,更加美觀。就是使用字符 @
去實現。代碼如下:
import time#定義一個計時器
def timeit(function):'''timeit函數負責返回一個wrapper,wrapper的參數要與原來的myfunc保持相同這樣一來,執行 myfunc=timeit(myfunc) myfunc完全等價于wrapperwrapper函數負責添加額外功能'''def wrapper():start = time.time()function()end =time.time()print(f'函數執行所花費的時間為:{end-start}')return wrapper#myfunc=timeit(myfunc) #注意,這里與前面的 “myfunc=timeit”是有所區別的哦#原來的函數myfunc
@timeit
def myfunc():print("我是函數myfunc")myfunc() #還和原來調用myfunc()一樣,但是達到了添加額外功能的效果
輸出結果同樣是:
我是函數myfunc
函數執行所花費的時間為:1.7881393432617188e-05
在上面的例子中,在定義myfunc函數的上面加了一個 @timeit,這與前面的寫法 myfunc = timeit(myfunc) 完全等價,
@有兩個重要的作用,第一:較少了代碼書寫量;第二:那就是讓我們的代碼看上去更有裝飾器的感覺,看起來更高端了。
總結:
在這個例子中,函數進入和退出時需要計時,這被稱為一個橫切面(Aspect),這種編程方式被稱為面向切面的編程(Aspect-Oriented Programming)。與傳統編程習慣的從上往下執行方式相比較而言,像是在函數執行的流程中橫向地插入了一段邏輯。在特定的業務領域里,能減少大量重復代碼。面向切面編程還有相當多的術語,這里就不多做介紹,感興趣的話可以去找找相關的資料(如果有需要,我后面也會抽時間專門寫一系列關于面向切面編程的文章,看我有沒有時間啦!)
2. 裝飾器的一般結構
為了能夠明確裝飾器的實現原理,這里給出一個關于裝飾器的 “一般模板” ,方便大家理解!但是,裝飾器作為一種設計模式,本身是沒有固定的設計模板的,語法也是相對較為靈活,沒有說一定要怎么寫才正確
模板如下:
def decorator(function):'''第一層函數為裝飾器名稱function:參數,即需要裝飾的函數return:返回值wrapper,為了保持與原函數參數一致'''def wrapper(*arg,**args):'''內層函數,這個函數實現“添加額外功能”的任務*arg,**args:參數保持與需要裝飾的函數參數一致,這里用*arg和**args代替'''#這里就是額外功能代碼function() #執行原函數#這里就是額外功能代碼return wrapper
一般就按照上面這個模板寫“裝飾器”函數,一般就不會出錯了。
四、裝飾器的各種花式實現
學過裝飾器的人都知道Python的閉包,關于“閉包”的詳細定義有各種版本,但我們經常看見這樣一句話,“Python的裝飾器就是一種閉包或者是Python的閉包其實就是裝飾器”,這句話在一定程度上是不正確的,但是這么說也可以(心里要明白二者的本質)。
本質:python閉包是裝飾器的真子集,即裝飾器是更加寬泛的概念,至于為什么,它們二者的區別和聯系,我會在
Python高級編程——裝飾器Decorator詳解(上篇)
中繼續講解python閉包和裝飾器的區別和聯系。
不僅如此,上面所實現的裝飾器是針對函數的,實際上Python的裝飾器可以是“函數”或者是“類”,而被裝飾的對象也可以是“函數”或者是“類”,這樣一來,就有四種搭配情況,即:
-
函數裝飾函數
-
函數裝飾類
-
類裝飾函數
-
類裝飾類
具體每一種怎么實現呢?其實他們的設計思想都是大同小異,只是實現細節略有不同,欲知詳細情況,且聽下回分解!!!
下一篇預告:
裝飾器與閉包的聯系和區別
四大類裝飾器的搭配實現