为什么需要装饰器
我们假设你的程序实现了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 >> property()
@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 {content}.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. hello toby! hello toby!
错误的函数签名和文档
装饰器装饰过的函数看上去名字没变,其实已经变了。
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的函数依次执行等等。
至此我所了解的装饰器已经讲完,但是还有一些内容没有提到,比如装饰类的装饰器。有机会再补充。谢谢观看。