詳解Python的裝飾器
Python中的裝飾器是你進入Python大門的一道坎,不管你跨不跨過去它都在那里。
為什么需要裝飾器
我們假設你的程序實現了say_hello()和say_goodbye()兩個函數。
- def say_hello():
- print "hello!"
- def say_goodbye():
- print "hello!" # bug here
- if __name__ == '__main__':
- say_hello()
- say_goodbye()
但是在實際調用中,我們發現程序出錯了,上面的代碼打印了兩個hello。經過調試你發現是say_goodbye()出錯了。老板要求調用每個方法前都要記錄進入函數的名稱,比如這樣:
- [DEBUG]: Enter say_hello()
- Hello!
- [DEBUG]: Enter say_goodbye()
- Goodbye!
好,小A是個畢業生,他是這樣實現的。
- def say_hello():
- print "[DEBUG]: enter say_hello()"
- print "hello!"
- def say_goodbye():
- print "[DEBUG]: enter say_goodbye()"
- print "hello!"
- if __name__ == '__main__':
- say_hello()
- say_goodbye()
很low吧? 嗯是的。小B工作有一段時間了,他告訴小A可以這樣寫。
- def debug():
- import inspect
- caller_name = inspect.stack()[1][3]
- print "[DEBUG]: enter {}()".format(caller_name)
- def say_hello():
- debug()
- print "hello!"
- def say_goodbye():
- debug()
- print "goodbye!"
- if __name__ == '__main__':
- say_hello()
- say_goodbye()
是不是好一點?那當然,但是每個業務函數里都要調用一下debug()函數,是不是很難受?萬一老板說say相關的函數不用debug,do相關的才需要呢?
那么裝飾器這時候應該登場了。
裝飾器本質上是一個Python函數,它可以讓其他函數在不需要做任何代碼變動的前提下增加額外功能,裝飾器的返回值也是一個函數對象。它經常用于有切面需求的場景,比如:插入日志、性能測試、事務處理、緩存、權限校驗等場景。裝飾器是解決這類問題的***設計,有了裝飾器,我們就可以抽離出大量與函數功能本身無關的雷同代碼并繼續重用。
概括的講,裝飾器的作用就是為已經存在的函數或對象添加額外的功能。
怎么寫一個裝飾器
在早些時候 (Python Version < 2.4,2004年以前),為一個函數添加額外功能的寫法是這樣的。
- def debug(func):
- def wrapper():
- print "[DEBUG]: enter {}()".format(func.__name__)
- return func()
- return wrapper
- def say_hello():
- print "hello!"
- say_hello = debug(say_hello) # 添加功能并保持原函數名不變
上面的debug函數其實已經是一個裝飾器了,它對原函數做了包裝并返回了另外一個函數,額外添加了一些功能。因為這樣寫實在不太優雅,在后面版本的Python中支持了@語法糖,下面代碼等同于早期的寫法。
- def debug(func):
- def wrapper():
- print "[DEBUG]: enter {}()".format(func.__name__)
- return func()
- return wrapper
- @debug
- def say_hello():
- print "hello!"
這是最簡單的裝飾器,但是有一個問題,如果被裝飾的函數需要傳入參數,那么這個裝飾器就壞了。因為返回的函數并不能接受參數,你可以指定裝飾器函數wrapper接受和原函數一樣的參數,比如:
- def debug(func):
- def wrapper(something): # 指定一毛一樣的參數
- print "[DEBUG]: enter {}()".format(func.__name__)
- return func(something)
- return wrapper # 返回包裝過函數
- @debug
- def say(something):
- print "hello {}!".format(something)
這樣你就解決了一個問題,但又多了N個問題。因為函數有千千萬,你只管你自己的函數,別人的函數參數是什么樣子,鬼知道?還好Python提供了可變參數*args和關鍵字參數**kwargs,有了這兩個參數,裝飾器就可以用于任意目標函數了。
- def debug(func):
- def wrapper(*args, **kwargs): # 指定宇宙無敵參數
- print "[DEBUG]: enter {}()".format(func.__name__)
- print 'Prepare and say...',
- return func(*args, **kwargs)
- return wrapper # 返回
- @debug
- def say(something):
- print "hello {}!".format(something)
至此,你已完全掌握初級的裝飾器寫法。
高級一點的裝飾器
帶參數的裝飾器和類裝飾器屬于進階的內容。在理解這些裝飾器之前,***對函數的閉包和裝飾器的接口約定有一定了解。(參見http://betacat.online/posts/p...
帶參數的裝飾器
假設我們前文的裝飾器需要完成的功能不僅僅是能在進入某個函數后打出log信息,而且還需指定log的級別,那么裝飾器就會是這樣的。
- def logging(level):
- def wrapper(func):
- def inner_wrapper(*args, **kwargs):
- print "[{level}]: enter function {func}()".format(
- level=level,
- func=func.__name__)
- return func(*args, **kwargs)
- return inner_wrapper
- return wrapper
- @logging(level='INFO')
- def say(something):
- print "say {}!".format(something)
- # 如果沒有使用@語法,等同于
- # say = logging(level='INFO')(say)
- @logging(level='DEBUG')
- def do(something):
- print "do {}...".format(something)
- if __name__ == '__main__':
- say('hello')
- do("my work")
是不是有一些暈?你可以這么理解,當帶參數的裝飾器被打在某個函數上時,比如@logging(level='DEBUG'),它其實是一個函數,會馬上被執行,只要這個它返回的結果是一個裝飾器時,那就沒問題。細細再體會一下。
基于類實現的裝飾器
裝飾器函數其實是這樣一個接口約束,它必須接受一個callable對象作為參數,然后返回一個callable對象。在Python中一般callable對象都是函數,但也有例外。只要某個對象重載了__call__()方法,那么這個對象就是callable的。
- class Test():
- def __call__(self):
- print 'call me!'
- t = Test()
- t() # call me
像__call__這樣前后都帶下劃線的方法在Python中被稱為內置方法,有時候也被稱為魔法方法。重載這些魔法方法一般會改變對象的內部行為。上面這個例子就讓一個類對象擁有了被調用的行為。
回到裝飾器上的概念上來,裝飾器要求接受一個callable對象,并返回一個callable對象(不太嚴謹,詳見后文)。那么用類來實現也是也可以的。我們可以讓類的構造函數__init__()接受一個函數,然后重載__call__()并返回一個函數,也可以達到裝飾器函數的效果。
- class logging(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)
- @logging
- def say(something):
- print "say {}!".format(something)
帶參數的類裝飾器
如果需要通過類形式實現帶參數的裝飾器,那么會比前面的例子稍微復雜一點。那么在構造函數里接受的就不是一個函數,而是傳入的參數。通過類把這些參數保存起來。然后在重載__call__方法是就需要接受一個函數并返回一個函數。
- class logging(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 #返回函數
- @logging(level='INFO')
- def say(something):
- print "say {}!".format(something)
內置的裝飾器
內置的裝飾器和普通的裝飾器原理是一樣的,只不過返回的不是函數,而是類對象,所以更難理解一些。
@property
在了解這個裝飾器前,你需要知道在不使用裝飾器怎么寫一個屬性。
- def getx(self):
- return self._x
- def setx(self, value):
- self._x = value
- def delx(self):
- del self._x
- # create a property
- x = property(getx, setx, delx, "I am doc for x property")
以上就是一個Python屬性的標準寫法,其實和Java挺像的,但是太羅嗦。有了@語法糖,能達到一樣的效果但看起來更簡單。
- @property
- def x(self): ...
- # 等同于
- def x(self): ...
- x = property(x)
屬性有三個裝飾器:setter, getter, deleter ,都是在property()的基礎上做了一些封裝,因為setter和deleter是property()的第二和第三個參數,不能直接套用@語法。getter裝飾器和不帶getter的屬性裝飾器效果是一樣的,估計只是為了湊數,本身沒有任何存在的意義。經過@property裝飾過的函數返回的不再是一個函數,而是一個property對象。
- >>> property()
- <property object at 0x10ff07940>
@staticmethod,@classmethod
有了@property裝飾器的了解,這兩個裝飾器的原理是差不多的。@staticmethod返回的是一個staticmethod類對象,而@classmethod返回的是一個classmethod類對象。他們都是調用的是各自的__init__()構造函數。
- class classmethod(object):
- """
- classmethod(function) -> method
- """
- def __init__(self, function): # for @classmethod decorator
- pass
- # ...
- class staticmethod(object):
- """
- staticmethod(function) -> method
- """
- def __init__(self, function): # for @staticmethod decorator
- pass
- # ...
裝飾器的@語法就等同調用了這兩個類的構造函數。
- class Foo(object):
- @staticmethod
- def bar():
- pass
- # 等同于 bar = staticmethod(bar)
至此,我們上文提到的裝飾器接口定義可以更加明確一些,裝飾器必須接受一個callable對象,其實它并不關心你返回什么,可以是另外一個callable對象(大部分情況),也可以是其他類對象,比如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>
錯誤的函數簽名和文檔
裝飾器裝飾過的函數看上去名字沒變,其實已經變了。
- 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__ # wrapper
為什么會這樣呢?只要你想想裝飾器的語法糖@代替的東西就明白了。@等同于這樣的寫法。
- say = logging(say)
logging其實返回的函數名字剛好是wrapper,那么上面的這個語句剛好就是把這個結果賦值給say,say的__name__自然也就是wrapper了,不僅僅是name,其他屬性也都是來自wrapper,比如doc,source等等。
使用標準庫里的functools.wraps,可以基本解決這個問題。
- 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__ # say
- print say.__doc__ # say something
看上去不錯!主要問題解決了,但其實還不太***。因為函數的簽名和源碼還是拿不到的。
- import inspect
- print inspect.getargspec(say) # failed
- print inspect.getsource(say) # failed
如果要徹底解決這個問題可以借用第三方包,比如wrapt。后文有介紹。
不能裝飾@staticmethod 或者 @classmethod
當你想把裝飾器用在一個靜態方法或者類方法時,不好意思,報錯了。
- 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
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是一個功能非常完善的包,用于實現各種你想到或者你沒想到的裝飾器。使用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的使用,建議查閱官方文檔,在此不在贅述。
小結
Python的裝飾器和Java的注解(Annotation)并不是同一回事,和C#中的特性(Attribute)也不一樣,完全是兩個概念。
裝飾器的理念是對原函數、對象的加強,相當于重新封裝,所以一般裝飾器函數都被命名為wrapper(),意義在于包裝。函數只有在被調用時才會發揮其作用。比如@logging裝飾器可以在函數執行時額外輸出日志,@cache裝飾過的函數可以緩存計算結果等等。
而注解和特性則是對目標函數或對象添加一些屬性,相當于將其分類。這些屬性可以通過反射拿到,在程序運行時對不同的特性函數或對象加以干預。比如帶有Setup的函數就當成準備步驟執行,或者找到所有帶有TestMethod的函數依次執行等等。
至此我所了解的裝飾器已經講完,但是還有一些內容沒有提到,比如裝飾類的裝飾器。有機會再補充。謝謝觀看。