[toc]
裝飾器是python的一種語法模式,本質(zhì)上是一種“函數(shù)的函數(shù)”。裝飾器的主要目的是增強(qiáng)函數(shù)的功能,當(dāng)多個函數(shù)需要重復(fù)使用同一個功能的時候,裝飾器可以更加簡介的進(jìn)行實(shí)現(xiàn)。裝飾器的底層邏輯是 函數(shù)的閉包,但這篇文章并不會涉及到。下面的內(nèi)容將會從一個最簡單、常見的案例出發(fā),逐步推導(dǎo)裝飾器的實(shí)現(xiàn)過程。
函數(shù) 是python中最基本的語法單元,假設(shè)有這么一個需求:
- 需要統(tǒng)計每個函數(shù)的消耗時間
- 需要在函數(shù)運(yùn)行時打印函數(shù)名
- 增強(qiáng)函數(shù)功能不能影響函數(shù)原有的參數(shù)和返回值
- 需要對消耗時間做邏輯判斷,如超過多少秒就記錄下來
如果沒有裝飾器該如何實(shí)現(xiàn)
在沒有裝飾器的前提下,對一個函數(shù)統(tǒng)計其運(yùn)行時間最簡單的辦法是直接修改原函數(shù)內(nèi)部代碼,比如在開始和結(jié)束后分別記錄時間戳并進(jìn)行相減運(yùn)算。這種模式的劣勢在于無法通用,每個函數(shù)都得增加重復(fù)代碼塊,且不便于以后的修改。那么很自然的邏輯是把“統(tǒng)計耗時”這個功能的代碼塊抽象出一個獨(dú)立的方法,如下:
import time
def timer(func):
start_time = time.time()
func()
end_time = time.time()
print(f"函數(shù){func.__name__} 消耗時間 {end_time-start_time}")
def func():
print('執(zhí)行fun 函數(shù)...')
time.sleep(1)
timer(func)
用簡易裝飾器如何實(shí)現(xiàn)
這種方法雖然不必要在每個函數(shù)進(jìn)行增加重復(fù)的代碼,但卻需要在函數(shù)的調(diào)用處修改,將原函數(shù)包裹起來。同樣,這種方法也不是很好的方式。那么,接下來的方法是,將函數(shù)作為參數(shù)傳入到另一個函數(shù)中去,經(jīng)過添加功能的修飾后,再把修飾后的復(fù)合函數(shù)返回出來作為一個新的復(fù)合函數(shù),這樣函數(shù)調(diào)用的時候,用這個新的復(fù)合函數(shù)即可。類似于給一顆糖包裹上一層糖衣。如下:
import time
def timer(func):
def wrapper():
start_time = time.time()
func()
end_time = time.time()
print(f"函數(shù){func.__name__} 消耗時間 {end_time-start_time}")
return wrapper
def func():
print('執(zhí)行fun 函數(shù)...')
time.sleep(1)
var = timer(func)
var()
# timer(func)() 上述兩行可以等價于這種語法
執(zhí)行fun 函數(shù)...
函數(shù)func 消耗時間 1.0023250579833984
這種把函數(shù)作為參數(shù)的方式傳入,經(jīng)過修飾后再返回的過程就是python裝飾器的基本模型。但如上代碼所示,這種方式看起來也不是簡潔的,每次調(diào)用的時候還得使用裝飾函數(shù)包裹一次,再用新的函數(shù)執(zhí)行。所以在python中這一步驟用@的語法糖進(jìn)行替代。
import time
def timer(func):
def wrapper():
start_time = time.time()
func()
end_time = time.time()
print(f"函數(shù){func.__name__} 消耗時間 {end_time-start_time}")
return wrapper
@timer
def func():
print('執(zhí)行fun 函數(shù)...')
time.sleep(1)
func()
執(zhí)行fun 函數(shù)...
函數(shù)func 消耗時間 1.0023250579833984
如上,函數(shù)fun新增的功能通過另外一個裝飾性函數(shù)實(shí)現(xiàn)每次調(diào)用核心函數(shù)的時候,因?yàn)檠b飾器語法糖的存在,總是先將核心函數(shù)作為參數(shù)傳入裝飾函數(shù)中,執(zhí)行修飾形后代碼邏輯。相比最初的思路,這種方式簡潔、閱讀性好,且不需要改動核心函數(shù)的邏輯。到此,python裝飾器的最簡過程已經(jīng)完成。
如果核心函數(shù)有返回值怎么辦
上述的模型中,核心函數(shù)是一個沒有輸入和輸出的函數(shù)。但如果核心函數(shù)有輸出,即返回值的時候,因?yàn)楹瘮?shù)生命周期的存在,經(jīng)過修飾后的復(fù)合函數(shù)并沒有接收到核心函數(shù)的返回值。所以如果用上述的最簡裝飾模型測試,調(diào)用函數(shù)的返回值為None。
所以,當(dāng)核心函數(shù)有返回值時,必須在裝飾函數(shù)的內(nèi)部也將返回值傳遞出來給復(fù)合函數(shù)。如下:
import time
def timer(func):
def wrapper():
start_time = time.time()
result = func()
end_time = time.time()
print(f"函數(shù){func.__name__} 消耗時間 {end_time-start_time}")
return result # 將核心函數(shù)的輸出值,丟給裝飾函數(shù)保管
return wrapper
@timer
def func():
print('執(zhí)行fun 函數(shù)...')
time.sleep(1)
return 1024 # 核心函數(shù)有輸出
result = func()
print(result)
執(zhí)行fun 函數(shù)...
函數(shù)func 消耗時間 1.0048329830169678
1024
如果核心函數(shù)有參數(shù)怎么辦
同理,如果核心函數(shù)不僅有返回值,也有輸入值,即參數(shù),那么修飾函數(shù)也必須接受該參數(shù)的輸入。但在案例中,修飾方法接收的是任意函數(shù),無法確定核心函數(shù)的參數(shù)類型。所以用萬能參數(shù)替代 *args, **kwargs
如下:
import time
def timer(func):
def wrapper(*args, **kwargs): # 核心函數(shù)攜帶參數(shù),丟給裝飾函數(shù)輸入
start_time = time.time()
result = func(*args, **kwargs) # 核心函數(shù)執(zhí)行過程接受參數(shù)
end_time = time.time()
print(f"函數(shù){func.__name__} 消耗時間 {end_time-start_time}")
return result # 將核心函數(shù)的輸出值,丟給裝飾函數(shù)保管
return wrapper
@timer
def func(x, y):
# 帶參數(shù)的核心函數(shù)
print('執(zhí)行fun 函數(shù)...')
time.sleep(1)
return x+y # 核心函數(shù)有輸出
result = func(1024, 996)
print(f"核心函數(shù)輸出:{result}")
print(f"調(diào)用函數(shù)名稱為:{func.__name__}")
執(zhí)行fun 函數(shù)...
函數(shù)func 消耗時間 1.0039029121398926
核心函數(shù)輸出:2020
調(diào)用函數(shù)名稱為:wrapper
在這一過程中,修飾函數(shù)并不關(guān)心核心函數(shù)的具體邏輯,也不關(guān)心參數(shù)是什么形式。所做的只是把核心函數(shù)的參數(shù)一起包裹進(jìn)來,并將核心函數(shù)的返回值打包丟給復(fù)合函數(shù)并丟出來。
關(guān)于復(fù)合函數(shù)的名字
python中函數(shù)有自己的名字,通過func.__name__
即可看到。上面的測試代碼顯示,復(fù)合函數(shù)的函數(shù)名是wrapper
。這樣的結(jié)果符合代碼邏輯,但不符合業(yè)務(wù)邏輯。例如糖衣包裹的棉花糖應(yīng)該叫棉花糖,而不是叫糖衣。為解決這個問題,可以在修飾器中返回wrapper
之前,把wrapper
的name重置為func
的name。如下:
# 函數(shù)名重置
wrapper.__name__ = function.__name__
wrapper.__doc__ = function.__doc__
return wrapper
作為裝飾器的通用功能,python內(nèi)部用另一個裝飾器進(jìn)行修飾來代替上面這兩行代碼。functools.wraps(function)
這個內(nèi)置的裝飾器的功能實(shí)際上等價于上訴兩行代碼。
所以上面的裝飾器經(jīng)過再次修飾后,最終的形式如下:
import time
import functools
def timer(func):
@functools.wraps(func) # 內(nèi)置裝飾器,讓被修飾后的函數(shù)的函數(shù)名與原函數(shù)一致
def wrapper(*args, **kwargs): # 核心函數(shù)攜帶參數(shù),丟給裝飾函數(shù)輸入
start_time = time.time()
result = func(*args, **kwargs) # 核心函數(shù)執(zhí)行過程接受參數(shù)
end_time = time.time()
print(f"函數(shù){func.__name__} 消耗時間 {end_time-start_time}")
return result # 將核心函數(shù)的輸出值,丟給裝飾函數(shù)保管
return wrapper
執(zhí)行fun 函數(shù)...
函數(shù)func 消耗時間 1.000427007675171
核心函數(shù)輸出:2020
調(diào)用函數(shù)名稱為:func
至此,案例中的需求已經(jīng)基本滿足。從頭來看完整的過程,即便不考慮底層邏輯,也可以從中發(fā)現(xiàn)一個完整的裝飾器實(shí)現(xiàn)模型:
- 需要給核心函數(shù)增加功能
- 將核心函數(shù)當(dāng)參數(shù)傳入修飾器
- 將核心函數(shù)的參數(shù)也傳入修飾器
- 核心函數(shù)的返回值在修飾中被拋出,交給復(fù)合函數(shù)托管
- 給核心函數(shù)添加裝飾器語法糖變成一個復(fù)合函數(shù)
- 調(diào)用核心函數(shù)實(shí)際上是調(diào)用復(fù)合函數(shù)
- 最后解決函數(shù)名的歸屬問題
裝飾器的基本模版如下:
如果裝飾器需要定制
上述的需求中還有一個對計時進(jìn)行邏輯判斷的需求,如果對于每個核心函數(shù)的判斷都一樣,那么在裝飾器中直接增加代碼邏輯即可。但如果每個核心函數(shù)的判斷不一樣,或者不同時間的要求不一樣,那么就需要定制裝飾器,把裝飾器再裝飾一下,也就是裝飾器的套娃。
import time
import functools
def timer_super(max_time=1): # 在原裝飾器上繼續(xù)套娃,增加裝飾器參數(shù)
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"函數(shù){func.__name__} 消耗時間 {end_time-start_time}")
if (end_time-start_time) > max_time:
print(f"超過最大耗時 {max_time}s")
return result
return wrapper
return timer
@timer_super(0.5)
def func(x, y):
print('執(zhí)行fun 函數(shù)...')
time.sleep(1)
return x+y # 核心函數(shù)有輸出
result = func(1024, 996)
print(f"核心函數(shù)輸出:{result}")
print(f"調(diào)用函數(shù)名稱為:{func.__name__}")
執(zhí)行fun 函數(shù)...
函數(shù)func 消耗時間 1.001241683959961
超過最大耗時 0.5s
核心函數(shù)輸出:2020
調(diào)用函數(shù)名稱為:func
總結(jié)
裝飾器的目的是重構(gòu)重復(fù)且通用代碼塊,使代碼得到簡化,提高閱讀性。上面的案例中,只是最常用的統(tǒng)計函數(shù)運(yùn)行時間。裝飾器還可以應(yīng)用到打印日志、登陸檢查、郵件發(fā)送等等具體的業(yè)務(wù)場景。同時,裝飾器的實(shí)現(xiàn)也不一定是函數(shù)的形式,也可以是裝飾器類。但無論是裝飾器函數(shù)還是裝飾器類,其基本邏輯和模版并沒有很大的區(qū)別。