Python装饰器详解

Admin 2018-10-09 16:06:51 Python

什么是装饰器?

在定义了许多的函数之后,我们想扩展这些函数的功能,如在函数执行前打印日志,但如果是一些通用的功能,修改每一个函数会显得不优雅且麻烦,那么如何在不修改当前函数调用又能扩展函数哪?答案就是定义一个装饰器,给每个函数增加功能,这种在代码运行时动态增加函数功能的方式就是装饰器(Decorator)。

简单的说装饰器是可以在不改变某个函数内部实现和原来调用方式的前提下对该函数增加一些附件的功能,提供了对该函数功能的扩展。

装饰器特性

  1. 能把被装饰的函数替换成其它函数

  2. 装饰器在加载模块时立即执行

  3. 在调用被装饰函数时执行

使用场景

  1. 日志写入

  2. 性能测试

  3. 事务处理

  4. 缓存

  5. 权限效验

由于Python中函数也是一个对象,而且函数对象可以被赋值给变量,所以函数可以像变量一样当做参数传递如:

def a():
   print "a"

def b(func):
   func()

b(a)

函数对象有一个__name__属性,可以拿到函数的名字:

>>> a.__name__
'a'

装饰器本质上也是一个函数或类,有了装饰器我们可以抽离大量与函数功能本身无关或重复的代码到装饰器中并继承重用。先看两个个简单的例子:

registry = []

def register(func):
   print("running register({})".format(func))
   registry.append(func)
   return func

@register
def f1():
    print('running f1')


@register
def f2():
    print('running f2')


def f3():
    print('running f3')

if __name__ == '__main__':
    print('running main')
    print('registry --> {}'.format(registry))
    f1()
    f2()
    f3()

register是一个加载时立即执行的装饰器,运行结果如下:

running register(<function f1 at 0x1038a0c80>)
running register(<function f2 at 0x1038a0cf8>)
running main
registry --> [<function f1 at 0x1038a0c80>, <function f2 at 0x1038a0cf8>]
running f1
running f2
running f3

注:@是Python装饰器的语法糖,放在函数开始定义的地方,可以省略赋值的一步。如:f = register(f3)  f()

简单的装饰器

def log(func):
    def wrapper(*args,**kwargs):
        print "{0} is running".format(func.__name__)
        return func()
    return wrapper

@log
def foo():
   print('i am foo',args)

log它把执行逻辑函数func包裹在其中,看起来像foo被log装饰一样,log返回的也是一个函数,这个函数是wrapper。(函数进入和退出时,被称为一个横切面,这种编程方式被称为面向切面的编程)

装饰器传参

如何给foo函数传入参数,如:

def foo(name):
   print('i am',name)

我们可以在定义wrapper函数时执行参数:

def wrapper(name):
        print "{0} is running".format(func.__name__)
        return func(name)
    return wrapper

如果需要传入多个参数可以使用 *args,**kwargs 如:

def wrapper(*args,**kwargs):
           print "{0} running level is {1}".format(func.__name__,level)
           return func(*args,**kwargs)
        return wrapper

带参数的装饰器

def logger(level): # 带参数的装饰器
    def decorator(func):
        def wrapper(*args,**kwargs):
           print "{0} running level is {1}".format(func.__name__,level)
           return func(*args,**kwargs)
        return wrapper
    return decorator

@logger("info")
def f4(name='f4'):
    print('i am %s' % name)

带参数的装饰器实际上是对原有装饰器的一个函数封装,并返回一个装饰器,可以将它理解为一个含有参数的闭包。

@logger(level="info")等价于@decorator

类装饰器

装饰器不仅可以是函数,还可以是类,相比函数装饰器,类装饰器具有灵活性、高内聚、封装性等优点。使用类封装器主要依靠类的__call__方法,当使用@形式将装饰器附加到函数上时,就会调用次方法。

class Foo(object):
   def __init__(self,func):
       self._func = func
   def __call__(self):
       print ('class decorator runing')
       self._func()
       print ('class decorator ending')
   
@Foo
def f5():
    print('i am f5')

functools.wraps

使用装饰器能极大的复用代码,但也有一个缺点就是原函数的元信息没有了,如函数的docstring和__name__,如:

def log(func):
   def wrapper(*args, **kwargs):
     print func.__name__      # 输出 'wrapper'
     print func.__doc__       # 输出 None
     return func(*args, **kwargs)
   return wrapper

# 函数
@log
def f(y):
   """does some math"""
   return y + y * y

log(f)

执行后,我们会发现,函数f被wrapper取代了,元信息也是wrapper的。functools.wraps就是解决此问题的,wraps本身也是一个装饰器,它能把原函数的元信息拷贝到装饰器里面的func函数中。

import functools

def logged(fun):
   @functools.wraps(fun)
   def wrapper(*args,**kw):
      print('run %s().' % fun.__name__)
      outcome = fun(*args,**kw)
      print('run %s() finished.' % fun.__name__)            
      return outcome        
   return wrapper

@logged
def f6(x):
    """does some math"""
    print('i am f6')
    return x + x * x

如何优化装饰器

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

wrapt

wrapt是一个功能非常完善的包,用于实现各种你想到的装饰器。

import wrapt

# without argument in decorator

@wrapt.decorator
def log(wrapped, instance, args, kwargs):  # instance is must
    print "[DEBUG]: enter {}()".format(wrapped.__name__)    
    return wrapped(*args, **kwargs)

@log
def say(something): 
    pass

总结

装饰器的理念是对原函数、对象的加强,相当于重新封装,所以一般装饰器函数都命名为wrapper,意义在于包装。函数只有在被调用时才会发挥其作用,如@log装饰器可以在函数执行时额外输出日志,@cache装饰过的函数可以缓存计算结果等等。

相关文章
最新推荐