裝飾器本質(zhì)上是一個函數(shù),該函數(shù)用來處理其他函數(shù),它可以讓其他函數(shù)在不需要修改代碼的前提下增加額外的功能,裝飾器的返回值也是一個函數(shù)對象。它經(jīng)常用于有切面需求的場景,比如:插入日志、性能測試、事務(wù)處理、緩存、權(quán)限校驗等應(yīng)用場景。裝飾器是解決這類問題的絕佳設(shè)計,有了裝飾器,我們就可以抽離出大量與函數(shù)功能本身無關(guān)的雷同代碼并繼續(xù)重用。概括的講,裝飾器的作用就是為已經(jīng)存在的對象添加額外的功能。
嚴格來說,裝飾器只是語法糖,裝飾器是可調(diào)用的對象,可以像常規(guī)的可調(diào)用對象那樣調(diào)用,特殊的地方是裝飾器的參數(shù)是一個函數(shù)
現(xiàn)在有一個新的需求,希望可以記錄下函數(shù)的執(zhí)行時間,于是在代碼中添加日志代碼:
import time
#遵守開放封閉原則
def foo():
start = time.time()
# print(start) # 1504698634.0291758從1970年1月1號到現(xiàn)在的秒數(shù),那年Unix誕生
time.sleep(3)
end = time.time()
print('spend %s'%(end - start))
foo()
bar()、bar2()也有類似的需求,怎么做?再在bar函數(shù)里調(diào)用時間函數(shù)?這樣就造成大量雷同的代碼,為了減少重復(fù)寫代碼,我們可以這樣做,重新定義一個函數(shù):專門設(shè)定時間:
import time
def show_time(func):
start_time=time.time()
func()
end_time=time.time()
print('spend %s'%(end_time-start_time))
def foo():
print('hello foo')
time.sleep(3)
show_time(foo)
但是這樣的話,你基礎(chǔ)平臺的函數(shù)修改了名字,容易被業(yè)務(wù)線的人投訴的,因為我們每次都要將一個函數(shù)作為參數(shù)傳遞給show_time函數(shù)。而且這種方式已經(jīng)破壞了原有的代碼邏輯結(jié)構(gòu),之前執(zhí)行業(yè)務(wù)邏輯時,執(zhí)行運行foo(),但是現(xiàn)在不得不改成show_time(foo)。那么有沒有更好的方式的呢?當(dāng)然有,答案就是裝飾器。
def show_time(f):
def inner():
start = time.time()
f()
end = time.time()
print('spend %s'%(end - start))
return inner
@show_time #foo=show_time(f)
def foo():
print('foo...')
time.sleep(1)
foo()
def bar():
print('bar...')
time.sleep(2)
bar()
輸出結(jié)果:
foo...
spend 1.0005607604980469
bar...
函數(shù)show_time就是裝飾器,它把真正的業(yè)務(wù)方法f包裹在函數(shù)里面,看起來像foo被上下時間函數(shù)裝飾了。在這個例子中,函數(shù)進入和退出時 ,被稱為一個橫切面(Aspect),這種編程方式被稱為面向切面的編程(Aspect-Oriented Programming)。
@符號是裝飾器的語法糖,在定義函數(shù)的時候使用,避免再一次賦值操作
裝飾器在Python使用如此方便都要歸因于Python的函數(shù)能像普通的對象一樣能作為參數(shù)傳遞給其他函數(shù),可以被賦值給其他變量,可以作為返回值,可以被定義在另外一個函數(shù)內(nèi)。
裝飾器有2個特性,一是可以把被裝飾的函數(shù)替換成其他函數(shù), 二是可以在加載模塊時候立即執(zhí)行
def decorate(func):
print('running decorate', func)
def decorate_inner():
print('running decorate_inner function')
return func()
return decorate_inner
@decorate
def func_1():
print('running func_1')
if __name__ == '__main__':
print(func_1)
#running decorate <function func_1 at 0x000001904743DEA0>
# <function decorate.<locals>.decorate_inner at 0x000001904743DF28>
func_1()
#running decorate_inner function
# running func_1
通過args 和 *kwargs 傳遞被修飾函數(shù)中的參數(shù)
def decorate(func):
def decorate_inner(*args, **kwargs):
print(type(args), type(kwargs))
print('args', args, 'kwargs', kwargs)
return func(*args, **kwargs)
return decorate_inner
@decorate
def func_1(*args, **kwargs):
print(args, kwargs)
if __name__ == '__main__':
func_1('1', '2', '3', para_1='1', para_2='2', para_3='3')
#返回結(jié)果
#<class 'tuple'> <class 'dict'>
# args ('1', '2', '3') kwargs {'para_1': '1', 'para_2': '2', 'para_3': '3'}
# ('1', '2', '3') {'para_1': '1', 'para_2': '2', 'para_3': '3'}
帶參數(shù)的被裝飾函數(shù)
import time
# 定長
def show_time(f):
def inner(x,y):
start = time.time()
f(x,y)
end = time.time()
print('spend %s'%(end - start))
return inner
@show_time
def add(a,b):
print(a+b)
time.sleep(1)
add(1,2)
不定長
import time
#不定長
def show_time(f):
def inner(*x,**y):
start = time.time()
f(*x,**y)
end = time.time()
print('spend %s'%(end - start))
return inner
@show_time
def add(*a,**b):
sum=0
for i in a:
sum+=i
print(sum)
time.sleep(1)
add(1,2,3,4)
帶參數(shù)的裝飾器
在上面的裝飾器調(diào)用中,比如@show_time,該裝飾器唯一的參數(shù)就是執(zhí)行業(yè)務(wù)的函數(shù)。裝飾器的語法允許我們在調(diào)用時,提供其它參數(shù),比如@decorator(a)。這樣,就為裝飾器的編寫和使用提供了更大的靈活性。
import time
def time_logger(flag=0):
def show_time(func):
def wrapper(*args, **kwargs):
start_time = time.time()
func(*args, **kwargs)
end_time = time.time()
print('spend %s' % (end_time - start_time))
if flag:
print('將這個操作的時間記錄到日志中')
return wrapper
return show_time
@time_logger(flag=1)
def add(*args, **kwargs):
time.sleep(1)
sum = 0
for i in args:
sum += i
print(sum)
add(1, 2, 5)
@time_logger(flag=1) 做了兩件事:
(1)time_logger(1):得到閉包函數(shù)show_time,里面保存環(huán)境變量flag
(2)@show_time?? :add=show_time(add)
上面的time_logger是允許帶參數(shù)的裝飾器。它實際上是對原有裝飾器的一個函數(shù)封裝,并返回一個裝飾器(一個含有參數(shù)的閉包函數(shù))。當(dāng)我 們使用@time_logger(1)調(diào)用的時候,Python能夠發(fā)現(xiàn)這一層的封裝,并把參數(shù)傳遞到裝飾器的環(huán)境中。
疊放裝飾器
執(zhí)行順序是什么
如果一個函數(shù)被多個裝飾器修飾,其實應(yīng)該是該函數(shù)先被最里面的裝飾器修飾后(下面例子中函數(shù)main()先被inner裝飾,變成新的函數(shù)),變成另一個函數(shù)后,再次被裝飾器修飾
def outer(func):
print('enter outer', func)
def wrapper():
print('running outer')
func()
return wrapper
def inner(func):
print('enter inner', func)
def wrapper():
print('running inner')
func()
return wrapper
@outer
@inner
def main():
print('running main')
if __name__ == '__main__':
main()
#返回結(jié)果
# enter inner <function main at 0x000001A9F2BCDF28>
# enter outer <function inner.<locals>.wrapper at 0x000001A9F2BD5048>
# running outer
# running inner
# running main
類裝飾器
相比函數(shù)裝飾器,類裝飾器具有靈活度大、高內(nèi)聚、封裝性等優(yōu)點。使用類裝飾器還可以依靠類內(nèi)部的__call__方法,當(dāng)使用 @ 形式將裝飾器附加到函數(shù)上時,就會調(diào)用此方法。
import time
class Foo(object):
def __init__(self, func):
self._func = func
def __call__(self):
start_time=time.time()
self._func()
end_time=time.time()
print('spend %s'%(end_time-start_time))
@Foo #bar=Foo(bar)
def bar():
print ('bar')
time.sleep(2)
bar()? #bar=Foo(bar)()>>>>>>>沒有嵌套關(guān)系了,直接active Foo的 __call__方法
標(biāo)準庫中有多種裝飾器
例如:裝飾方法的函數(shù)有property, classmethod, staticmethod; functools模塊中的lru_cache, singledispatch,? wraps 等等
from functools import lru_cache
from functools import singledispatch
from functools import wraps
functools.wraps使用裝飾器極大地復(fù)用了代碼,但是他有一個缺點就是原函數(shù)的元信息不見了,比如函數(shù)的docstring、__name__、參數(shù)列表,先看例子:
def foo():
print("hello foo")
print(foo.__name__)# foo
def logged(func):
def wrapper(*args, **kwargs):
print (func.__name__ + " was called")
return func(*args, **kwargs)
return wrapper
@logged
def cal(x):
resul=x + x * x
print(resul)
cal(2)
#6
#cal was called
print(cal.__name__)# wrapper
print(cal.__doc__)#None
#函數(shù)f被wrapper取代了,當(dāng)然它的docstring,__name__就是變成了wrapper函數(shù)的信息了。
好在我們有functools.wraps,wraps本身也是一個裝飾器,它能把原函數(shù)的元信息拷貝到裝飾器函數(shù)中,這使得裝飾器函數(shù)也有和原函數(shù)一樣的元信息了。
from functools import wraps
def logged(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(func.__name__ + " was called")
return func(*args, **kwargs)
return wrapper
@logged
def cal(x):
return x + x * x
print(cal.__name__) # cal
使用裝飾器會產(chǎn)生我們可能不希望出現(xiàn)的副作用, 例如:改變被修飾函數(shù)名稱,對于調(diào)試器或者對象序列化器等需要使用內(nèi)省機制的那些工具,可能會無法正常運行;
其實調(diào)用裝飾器后,會將同一個作用域中原來函數(shù)同名的那個變量(例如下面的func_1),重新賦值為裝飾器返回的對象;使用@wraps后,會把與內(nèi)部函數(shù)(被修飾函數(shù),例如下面的func_1)相關(guān)的重要元數(shù)據(jù)全部復(fù)制到外圍函數(shù)(例如下面的decorate_inner)
from functools import wraps
def decorate(func):
print('running decorate', func)
@wraps(func)
def decorate_inner():
print('running decorate_inner function', decorate_inner)
return func()
return decorate_inner
@decorate
def func_1():
print('running func_1', func_1)
if __name__ == '__main__':
func_1()
#輸出結(jié)果
#running decorate <function func_1 at 0x0000023E8DBD78C8>
# running decorate_inner function <function func_1 at 0x0000023E8DBD7950>
# running func_1 <function func_1 at 0x0000023E8DBD7950>
文章同步發(fā)布:?https://www.geek-share.com/detail/2784034826.html
參考文章: