裝飾器是 Python 的一個重要部分。它是修改其餘函數的功能的函數,有助於讓咱們的代碼更簡短html
裝飾器本質上是一個Python函數,它可讓其餘函數在不須要作任何代碼變更的前提下增長額外功能,裝飾器的返回值也是一個函數對象。它常常用於有切面需求的場景,好比:插入日誌、性能測試、事務處理、緩存、權限校驗等場景。裝飾器是解決這類問題的絕佳設計,有了裝飾器,咱們就能夠抽離出大量與函數功能自己無關的雷同代碼並繼續重用。python
歸納的講,裝飾器的做用就是爲已經存在的函數或對象添加額外的功能。程序員
咱們假設你的程序實現了func_enter()
和func_quit()
兩個函數。緩存
def func_enter():
print "enter!"
def func_quit():
print "enter!" # bug here
if __name__ == '__main__':
func_enter()
func_quit()
運行結果:bash
enter! enter! (wda_python) bash-3.2$
可是在實際調用中, 咱們發現程序出錯了, 上面打印了2個enter。通過調試咱們發現是func_quit()出錯了app
如今假如要求調用每一個方法前都要記錄進入函數的名稱, 好比這樣:函數
[DEBUG]: enter func_enter() enter! [DEBUG]: enter func_quit() enter!
一種最直白簡單的方式是這樣寫:性能
def func_enter(): print "[DEBUG]: enter func_enter()" print "enter!" def func_quit(): print "[DEBUG]: enter func_quit()" print "enter!" # bug here if __name__ == '__main__': func_enter() func_quit()
可是很low對吧, 咱們能夠試着這樣寫:測試
def debug(): import inspect caller_name = inspect.stack()[1][3] print '[BEBUG]: enter {}()'.format(caller_name) def func_enter(): debug() print "enter!" def func_quit(): debug() print "enter!" # bug here if __name__ == '__main__': func_enter() func_quit()
看起來會好一點, 可是每一個函數都要調用一次debug()函數,仍是不太夠, 萬一若是又改需求進出不打印調用者了, 其餘地方或者函數在打印, 又要大改優化
怎麼辦呢? 這個時候裝飾器就能夠派上用場了
咱們來看一個例子
def debug(func): def wrapper(): print '[DEBUG]: enter {}()'.format(func.__name__) return func() return wrapper @debug def func_enter(): print "enter!" @debug def func_quit(): print "enter!" # bug here if __name__ == '__main__': func_enter() func_quit()
運行結果:
[DEBUG]: enter func_enter() enter! [DEBUG]: enter func_quit() enter! (wda_python) bash-3.2$
這是一個最簡單的裝飾器, 可是有個問題, 若是被裝飾的函數須要傳入參數, 那麼這個裝飾器就壞了,由於返回的函數並不能接受參數
這裏能夠指定裝飾器函數wrapper接受和原函數同樣的參數, 好比:
#coding: utf-8 def debug(func): def wrapper(something): # 這裏指定同樣的參數 print '[DEBUG]: enter {}()'.format(func.__name__) return func(something) return wrapper # 返回包裝過的函數 @debug def func_enter(something): print "enter {}!".format(something) @debug def func_quit(something): print "enter {}!".format(something) # bug here if __name__ == '__main__': func_enter("enter_func") func_quit("quit_func")
運行結果:
[DEBUG]: enter func_enter() enter enter_func! [DEBUG]: enter func_quit() enter quit_func!
這樣解決了傳參數的問題, 可是這裏有個很大的問題是這裏只適配了咱們的func_enter和func_quit函數的參數, 若是要用來去裝飾其餘帶參數的函數呢?
還好python提供可變參數*args和關鍵字參數**kwargs, 有這兩個參數裝飾器就能夠用於任意目標函數了
#coding: utf-8 def debug(func): def wrapper(*args, **kwargs): # 這裏指定同樣的參數 print '[DEBUG]: enter {}()'.format(func.__name__) return func(*args, **kwargs) return wrapper # 返回包裝過的函數 @debug def func_enter(something): print "enter {}!".format(something) @debug def func_quit(something): print "enter {}!".format(something) # bug here if __name__ == '__main__': func_enter("enter_func") func_quit("quit_func")
運行結果:
[DEBUG]: enter func_enter() enter enter_func! [DEBUG]: enter func_quit() enter quit_func! (wda_python) bash-3.2$
若是前面咱們的裝飾器須要完成的功能不只僅是能在進入某個函數後打印出調用信息,還要指定log級別, 那麼裝飾器能夠是這樣:
#coding: utf-8 def debug(level): def wrapper(func): def inner_wrapper(*args, **kwargs): print '[{level}]: enter {func}()'.format(level=level,func=func.__name__) return func(*args, **kwargs) return inner_wrapper return wrapper @debug(level='Debug') def func_enter(something): print "enter {}!".format(something) @debug(level='Debug') def func_quit(something): print "enter {}!".format(something) # bug here if __name__ == '__main__': func_enter("enter_func") func_quit("quit_func")
運行結果:
[Debug]: enter func_enter() enter enter_func! [Debug]: enter func_quit() enter quit_func! (wda_python) bash-3.2$
裝飾器函數實際上是這樣一個接口約束,它必須接受一個callable對象做爲參數,而後返回一個callable對象。在Python中通常callable對象都是函數,但也有例外。只要某個對象重載了__call__()
方法,那麼這個對象就是callable的。
class Test(): def __call__(self, *args, **kwargs): print 'call me!' t = Test() t()
運行結果:
call me! (wda_python) bash-3.2$
像__call__
這樣先後都帶下劃線的方法在Python中被稱爲內置方法,有時候也被稱爲魔法方法。重載這些魔法方法通常會改變對象的內部行爲。上面這個例子就讓一個類對象擁有了被調用的行爲。
回到裝飾器上的概念上來,裝飾器要求接受一個callable對象,並返回一個callable對象(不太嚴謹,詳見後文)。那麼用類來實現也是也能夠的。咱們可讓類的構造函數__init__()
接受一個函數,而後重載__call__()
並返回一個函數,也能夠達到裝飾器函數的效果。
class Debug_info(object): def __init__(self, func): self.func = func def __call__(self, *args, **kwargs): print "[DEBUG]: enter function {func}()".format(func=self.func.__name__) return self.func(*args, **kwargs) @Debug_info def func_enter(something): print 'enter {}!'.format(something) if __name__ == '__main__': func_enter("enter_func")
運行結果:
[DEBUG]: enter function func_enter() enter enter_func! (wda_python) bash-3.2$
若是須要經過類形式實現帶參數的裝飾器,那麼會比前面的例子稍微複雜一點。那麼在構造函數裏接受的就不是一個函數,而是傳入的參數。經過類把這些參數保存起來。而後在重載__call__
方法是就須要接受一個函數並返回一個函數。
#coding: utf-8 class Debug_info(object): def __init__(self, level='INFO'): self.level= level def __call__(self, func): # 接受函數 def wrapper(*args, **kwargs): print "[{level}]: enter function {func}()".format(level=self.level,func=func.__name__) func(*args, **kwargs) return wrapper @Debug_info(level='INFO') def func_enter(something): print 'enter {}!'.format(something) if __name__ == '__main__': func_enter("enter_func")
運行結果:
[INFO]: enter function func_enter() enter enter_func! (wda_python) bash-3.2$
在綁定屬性時,若是咱們直接把屬性暴露出去,雖然寫起來很簡單,可是,沒辦法檢查參數,致使能夠把成績隨便改:
s = Student() s.score = 9999
這顯然不合邏輯。爲了限制score的範圍,能夠經過一個set_score()
方法來設置成績,再經過一個get_score()
來獲取成績,這樣,在set_score()
方法裏,就能夠檢查參數:
class Student(object): def get_score(self): return self._score def set_score(self, value): if not isinstance(value, int): raise ValueError('score must be an integer!') if value < 0 or value > 100: raise ValueError('score must between 0 ~ 100!') self._score = value
如今,對任意的Student實例進行操做,就不能爲所欲爲地設置score了:
>>> s = Student() >>> s.set_score(60) # ok! >>> s.get_score() 60 >>> s.set_score(9999) Traceback (most recent call last): ... ValueError: score must between 0 ~ 100!
可是,上面的調用方法又略顯複雜,沒有直接用屬性這麼直接簡單。
有沒有既能檢查參數,又能夠用相似屬性這樣簡單的方式來訪問類的變量呢?對於追求完美的Python程序員來講,這是必需要作到的!
還記得裝飾器(decorator)能夠給函數動態加上功能嗎?對於類的方法,裝飾器同樣起做用。Python內置的@property
裝飾器就是負責把一個方法變成屬性調用的:
class Student(object): @property def score(self): return self._score @score.setter def score(self, value): if not isinstance(value, int): raise ValueError('score must be an integer!') if value < 0 or value > 100: raise ValueError('score must between 0 ~ 100!') self._score = value
@property
的實現比較複雜,咱們先考察如何使用。把一個getter方法變成屬性,只須要加上@property
就能夠了,此時,@property
自己又建立了另外一個裝飾器@score.setter
,負責把一個setter方法變成屬性賦值,因而,咱們就擁有一個可控的屬性操做:
>>> s = Student() >>> s.score = 60 # OK,實際轉化爲s.set_score(60) >>> s.score # OK,實際轉化爲s.get_score() 60 >>> s.score = 9999 Traceback (most recent call last): ... ValueError: score must between 0 ~ 100!
注意到這個神奇的@property
,咱們在對實例屬性操做的時候,就知道該屬性極可能不是直接暴露的,而是經過getter和setter方法來實現的。
還能夠定義只讀屬性,只定義getter方法,不定義setter方法就是一個只讀屬性:
class Student(object): @property def birth(self): return self._birth @birth.setter def birth(self, value): self._birth = value @property def age(self): return 2014 - self._birth
上面的birth
是可讀寫屬性,而age
就是一個只讀屬性,由於age
能夠根據birth
和當前時間計算出來。
@property
普遍應用在類的定義中,可讓調用者寫出簡短的代碼,同時保證對參數進行必要的檢查,這樣,程序運行時就減小了出錯的可能性。
裝飾器可讓你代碼更加優雅,減小重複,但也不全是優勢,也會帶來一些問題。
讓咱們直接看示例代碼:
def html_tags(tag_name): print 'begin outer function.' def wrapper_(func): print "begin of inner wrapper function." def wrapper(*args, **kwargs): content = func(*args, **kwargs) print "<{tag}>{content}</{tag}>".format(tag=tag_name, content=content) print 'end of inner wrapper function.' return wrapper print 'end of outer function' return wrapper_ @html_tags('b') def hello(name='Toby'): return 'Hello {}!'.format(name) hello() hello()
在裝飾器中我在各個可能的位置都加上了print語句,用於記錄被調用的狀況。你知道他們最後打印出來的順序嗎?若是你內心沒底,那麼最好不要在裝飾器函數以外添加邏輯功能,不然這個裝飾器就不受你控制了。如下是輸出結果:
begin outer function. end of outer function begin of inner wrapper function. end of inner wrapper function. <b>Hello Toby!</b> <b>Hello Toby!</b> (wda_python) bash-3.2$
裝飾器裝飾過的函數看上去名字沒變,其實已經變了。
import datetime
def logging(func):
def wrapper(*args, **kwargs):
"""print log before a function."""
print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
return func(*args, **kwargs)
return wrapper
@logging
def say(something):
"""say something"""
print "say {}!".format(something)
print say.__name__
print say.__doc__
運行結果:
wrapper print log before a function. (wda_python) bash-3.2$
爲何會這樣呢?只要你想一想裝飾器的語法糖@代替的東西就明白了。@等同於這樣的寫法。
say = logging(say)
logging
其實返回的函數名字恰好是wrapper
,那麼上面的這個語句恰好就是把這個結果賦值給say
,say
的__name__
天然也就是wrapper
了,不只僅是name
,其餘屬性也都是來自wrapper
,好比doc
,source
等等。
使用標準庫裏的functools.wraps
,能夠基本解決這個問題。
import datetime from functools import wraps def logging(func): @wraps(func) def wrapper(*args, **kwargs): """print log before a function.""" print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__) return func(*args, **kwargs) return wrapper @logging def say(something): """say something""" print "say {}!".format(something) print say.__name__ print say.__doc__
運行結果:
say say something (wda_python) bash-3.2$
可是其實還不太完美, 由於函數的簽名和源碼仍是拿不到
import datetime from functools import wraps def logging(func): @wraps(func) def wrapper(*args, **kwargs): """print log before a function.""" print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__) return func(*args, **kwargs) return wrapper @logging def say(something): """say something""" print "say {}!".format(something) print say.__name__ print say.__doc__ import inspect print inspect.getargspec(say) print inspect.getsource(say)
運行結果:
say say something ArgSpec(args=[], varargs='args', keywords='kwargs', defaults=None) @wraps(func) def wrapper(*args, **kwargs): """print log before a function.""" print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__) return func(*args, **kwargs) (wda_python) bash-3.2$
若是要完全解決這個問題能夠借用第三方包,好比wrapt, 後文有介紹
當你想把裝飾器用在一個靜態方法或者類方法時,很差意思,報錯了。
class Car(object): def __init__(self, model): self.model = model @logging # 裝飾實例方法,OK def run(self): print "{} is running!".format(self.model) @logging # 裝飾靜態方法,Failed @staticmethod def check_model_for(obj): if isinstance(obj, Car): print "The model of your car is {}".format(obj.model) else: print "{} is not a car!".format(obj) """ Traceback (most recent call last): ... File "example_4.py", line 10, in logging @wraps(func) File "C:\Python27\lib\functools.py", line 33, in update_wrapper setattr(wrapper, attr, getattr(wrapped, attr)) AttributeError: 'staticmethod' object has no attribute '__module__' """
前面已經解釋了@staticmethod
這個裝飾器,其實它返回的並非一個callable對象,而是一個staticmethod
對象,那麼它是不符合裝飾器要求的(好比傳入一個callable對象),你天然不能在它之上再加別的裝飾器。要解決這個問題很簡單,只要把你的裝飾器放在@staticmethod
以前就行了,由於你的裝飾器返回的仍是一個正常的函數,而後再加上一個@staticmethod
是不會出問題的。
class Car(object): def __init__(self, model): self.model = model @staticmethod @logging # 在@staticmethod以前裝飾,OK def check_model_for(obj): pass
嵌套的裝飾函數不太直觀,咱們可使用第三方包類改進這樣的狀況,讓裝飾器函數可讀性更好。
decorator.py是一個很是簡單的裝飾器增強包。你能夠很直觀的先定義包裝函數wrapper()
,再使用decorate(func, wrapper)
方法就能夠完成一個裝飾器。
from decorator import decorate def wrapper(func, *args, **kwargs): """print log before a function.""" print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__) return func(*args, **kwargs) def logging(func): return decorate(func, wrapper) # 用wrapper裝飾func
你也可使用它自帶的@decorator
裝飾器來完成你的裝飾器。
from decorator import decorator @decorator def logging(func, *args, **kwargs): print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__) return func(*args, **kwargs)
decorator.py
實現的裝飾器能完整保留原函數的name
,doc
和args
,惟一有問題的就是inspect.getsource(func)
返回的仍是裝飾器的源代碼,你須要改爲inspect.getsource(func.__wrapped__)
。
wrapt是一個功能很是完善的包,用於實現各類你想到或者你沒想到的裝飾器。使用wrapt實現的裝飾器你不須要擔憂以前inspect中遇到的全部問題,由於它都幫你處理了,甚至inspect.getsource(func)
也準確無誤。
import wrapt # without argument in decorator @wrapt.decorator def logging(wrapped, instance, args, kwargs): # instance is must print "[DEBUG]: enter {}()".format(wrapped.__name__) return wrapped(*args, **kwargs) @logging def say(something): pass
使用wrapt你只須要定義一個裝飾器函數,可是函數簽名是固定的,必須是(wrapped, instance, args, kwargs)
,注意第二個參數instance
是必須的,就算你不用它。當裝飾器裝飾在不一樣位置時它將獲得不一樣的值,好比裝飾在類實例方法時你能夠拿到這個類實例。根據instance
的值你可以更加靈活的調整你的裝飾器。另外,args
和kwargs
也是固定的,注意前面沒有星號。在裝飾器內部調用原函數時才帶星號。
若是你須要使用wrapt寫一個帶參數的裝飾器,能夠這樣寫:
def logging(level): @wrapt.decorator def wrapper(wrapped, instance, args, kwargs): print "[{}]: enter {}()".format(level, wrapped.__name__) return wrapped(*args, **kwargs) return wrapper @logging(level="INFO") def do(work): pass
關於wrapt的使用,建議查閱官方文檔,在此不在贅述。