Python裝飾器
- 定義了一個函數(shù)
- 想在運行時動態(tài)增加功能
- 又不想改動函數(shù)本身代碼
通過高階函數(shù)返回新函數(shù)
def f1(x):
return x*2
def new_fn(f): # new_fn()函數(shù)就是裝飾器函數(shù)
def fn(x):
print 'call' + f._name_ + '()'
return f(x)
return fn
如何調(diào)用new_fn()裝飾器函數(shù)
方法1:
g1 = new_fn(f1)
print g1(5)
方法2:
f1 = new_fn(f1)
print f1(5)
# f1的原始定義函數(shù)被徹底隱藏了
Python內(nèi)置的@語法就是為了簡化裝飾器調(diào)用
@new_fn
def f1(x):
return x*2
上述的代碼等價于下面的代碼:
def f1(x):
return x*2
f1 = new_fn(f1)
裝飾器的作用:
可以極大地簡化代碼,避免每個函數(shù)編寫重復(fù)性代碼
Python中編寫無參數(shù)decorator
Python的 decorator 本質(zhì)上就是一個高階函數(shù),它接收一個函數(shù)作為參數(shù)肋杖,然后,返回一個新函數(shù)赃梧。
使用 decorator 用Python提供的 @ 語法玫坛,這樣可以避免手動編寫 f = decorate(f) 這樣的代碼。
考察一個@log的定義:
def log(f):
def fn(x):
print 'call ' + f.__name__ + '()...'
return f(x)
return fn
對于階乘函數(shù)签夭,@log工作得很好:
@log
def factorial(n):
return reduce(lambda x,y: x*y, range(1, n+1))
print factorial(10)
結(jié)果:
call factorial()...
3628800
但是,對于參數(shù)不是一個的函數(shù)椎侠,調(diào)用將報錯:
@log
def add(x, y):
return x + y
print add(1, 2)
結(jié)果:
Traceback (most recent call last):
File "test.py", line 15, in <module>
print add(1,2)
TypeError: fn() takes exactly 1 argument (2 given)
因為 add()
函數(shù)需要傳入兩個參數(shù)第租,但是 @log
寫死了只含一個參數(shù)的返回函數(shù)。
要讓 @log
自適應(yīng)任何參數(shù)定義的函數(shù)我纪,可以利用Python的 *args
和 **kw
慎宾,保證任意個數(shù)的參數(shù)總是能正常調(diào)用:
def log(f):
def fn(*args, **kw):
print 'call ' + f.__name__ + '()...'
return f(*args, **kw)
return fn
現(xiàn)在丐吓,對于任意函數(shù),@log
都能正常工作趟据。
python中編寫帶參數(shù)decorator
考察上一節(jié)的 @log
裝飾器:
def log(f):
def fn(x):
print 'call ' + f.__name__ + '()...'
return f(x)
return fn
發(fā)現(xiàn)對于被裝飾的函數(shù)券犁,log打印的語句是不能變的(除了函數(shù)名)。
如果有的函數(shù)非常重要汹碱,希望打印出'[INFO] call xxx()...'粘衬,有的函數(shù)不太重要,希望打印出'[DEBUG] call xxx()...'咳促,這時稚新,log函數(shù)本身就需要傳入'INFO'或'DEBUG'這樣的參數(shù),類似這樣:
@log('DEBUG')
def my_func():
pass
把上面的定義翻譯成高階函數(shù)的調(diào)用跪腹,就是:
my_func = log('DEBUG')(my_func)
上面的語句看上去還是比較繞褂删,再展開一下:
log_decorator = log('DEBUG')
my_func = log_decorator(my_func)
上面的語句又相當于:
log_decorator = log('DEBUG')
@log_decorator
def my_func():
pass
所以,帶參數(shù)的log函數(shù)首先返回一個decorator函數(shù)冲茸,再讓這個decorator函數(shù)接收my_func并返回新函數(shù):
def log(prefix):
def log_decorator(f):
def wrapper(*args, **kw):
print '[%s] %s()...' % (prefix, f.__name__)
return f(*args, **kw)
return wrapper
return log_decorator
@log('DEBUG')
def test():
pass
print test()
執(zhí)行結(jié)果:
[DEBUG] test()...
None
對于這種3層嵌套的decorator定義笤妙,你可以先把它拆開:
# 標準decorator:
def log_decorator(f):
def wrapper(*args, **kw):
print '[%s] %s()...' % (prefix, f.__name__)
return f(*args, **kw)
return wrapper
return log_decorator
# 返回decorator:
def log(prefix):
return log_decorator(f)
拆開以后會發(fā)現(xiàn),調(diào)用會失敗噪裕,因為在3層嵌套的decorator
定義中蹲盘,最內(nèi)層的wrapper
引用了最外層的參數(shù)prefix
,所以膳音,把一個閉包拆成普通的函數(shù)調(diào)用會比較困難召衔。不支持閉包的編程語言要實現(xiàn)同樣的功能就需要更多的代碼。
Python中完善decorator
@decorator
可以動態(tài)實現(xiàn)函數(shù)功能的增加祭陷,但是苍凛,經(jīng)過@decorator
“改造”后的函數(shù),和原函數(shù)相比兵志,除了功能多一點外醇蝴,有沒有其它不同的地方?
在沒有decorator的情況下想罕,打印函數(shù)名:
def f1(x):
pass
print f1.__name__
輸出:
f1
有decorator的情況下悠栓,再打印函數(shù)名:
def log(f):
def wrapper(*args, **kw):
print 'call...'
return f(*args, **kw)
return wrapper
@log
def f2(x):
pass
print f2.__name__
輸出:
wrapper
可見,由于decorator
返回的新函數(shù)函數(shù)名已經(jīng)不是'f2'
按价,而是@log
內(nèi)部定義的'wrapper'
惭适。這對于那些依賴函數(shù)名的代碼就會失效。decorator
還改變了函數(shù)的__doc__
等其它屬性楼镐。如果要讓調(diào)用者看不出一個函數(shù)經(jīng)過了@decorator
的“改造”癞志,就需要把原函數(shù)的一些屬性復(fù)制到新函數(shù)中:
def log(f):
def wrapper(*args, **kw):
print 'call...'
return f(*args, **kw)
wrapper.__name__ = f.__name__
wrapper.__doc__ = f.__doc__
return wrapper
這樣寫decorator
很不方便,因為我們也很難把原函數(shù)的所有必要屬性都一個一個復(fù)制到新函數(shù)上框产,所以Python內(nèi)置的functools
可以用來自動化完成這個“復(fù)制”的任務(wù):
import functools
def log(f):
@functools.wraps(f)
def wrapper(*args, **kw):
print 'call...'
return f(*args, **kw)
return wrapper
最后需要指出凄杯,由于我們把原函數(shù)簽名改成了(*args, **kw)
错洁,因此,無法獲得原函數(shù)的原始參數(shù)信息戒突。即便我們采用固定參數(shù)來裝飾只有一個參數(shù)的函數(shù):
def log(f):
@functools.wraps(f)
def wrapper(x):
print 'call...'
return f(x)
return wrapper
也可能改變原函數(shù)的參數(shù)名屯碴,因為新函數(shù)的參數(shù)名始終是'x'
,原函數(shù)定義的參數(shù)名不一定叫'x'
妖谴。
2018.11.14更新:
裝飾器實際上是一個函數(shù)
有兩個特別之處:
- 參數(shù)是一個函數(shù)
- 返回值是一個函數(shù)
- 裝飾器使用的是通過
@
符號窿锉,放在函數(shù)上面 - 裝飾器中定義的函數(shù)酌摇,要使用
*args
,**kwargs
兩對兄弟的組合膝舅,并且在這個函數(shù)中執(zhí)行原始函數(shù)的時候也要把*args
,**kwargs
傳過去。 - 需要使用
functools.wraps
在裝飾器中的函數(shù)上把傳進來的這個函數(shù)經(jīng)行一個包裹窑多,這樣就不會丟失原來的函數(shù)的name等屬性
# import functools
def login_required(f):
"""login_required"""
def wrapper(*args,**kwargs):
"""wrapper"""
return f(*args,**kwargs)
return wrapper
@login_required
def t():
"""T"""
print('tt')
print(t.__name__)
print(t.__doc__)
t()
#輸出:
wrapper
wrapper
tt
import functools
def login_required(f):
""" login_required"""
@functools.wraps(f)
def wrapper(*args, **kwargs):
"""wrapper"""
return f(*args, **kwargs)
return wrapper
@ login_required # t = login_required(t)
def t():
"""T"""
print('tt')
print(t.__name__)
print(t.__doc__)
t()
輸出:
t
T
tt
裝飾器順序
一個函數(shù)還可以同時定義多個裝飾器仍稀,比如:
@a
@b
@c
def f ():
pass
它的執(zhí)行順序是從里到外,最先調(diào)用最里層的裝飾器埂息,最后調(diào)用最外層的裝飾器技潘,它等效于
f = a(b(c(f)))