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
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的級別,那麼裝飾器就會是這樣的。 ='DEBUG'),它其實是一個函數,會馬上被執行,只要這個它回傳的結果是一個裝飾器時,那就沒問題。裝飾器函數其實是這樣一個介面約束,它必須接受一個callable物件作為參數,然後回傳一個callable物件。方法,那麼這個物件就是callable的。的內部行為。後文)。
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")
帶參數的類別裝飾器
如果需要透過類別形式實現帶有參數的裝飾器,那麼會比前面的例子稍微複雜一點。參數。的,只不過返回的不是函數,而是類對象,所以更難理解一些。
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的使用,建议查阅官方文档,在此不在赘述。
http://wrapt.readthedocs.io/e...
小结
Python的裝飾器和Java的註解(Annotation)並不是同一回事,和C#中的特性(Attribute)也不一樣,完全是兩個概念。
裝飾器的理念是對原始函數、物件的加強,相當於重新封裝,所以一般裝飾器函數都被命名為wrapper(),意義在於包裝。函數只有在被呼叫時才會發揮其作用。例如@logging裝飾器可以在函數執行時額外輸出日誌,@cache裝飾過的函數可以快取計算結果等等。
而註解和特性則是對目標函數或物件添加一些屬性,相當於將其分類。這些屬性可以透過反射拿到,在程式運行時對不同的特性函數或物件加以乾預。例如有Setup的函數就當成準備步驟執行,或是找到所有有TestMethod的函數依序執行等等。
至此我所了解的裝飾器已經講完,但是還有一些內容沒有提到,例如裝飾類的裝飾器。有機會再補充。謝謝觀看。