AnthonyZero's Bolg

理解使用python装饰器

Alt text

概述

装饰器本质上是一个 Python 函数或类,它可以让其他函数或类在不需要做任何代码修改的前提下增加额外功能,装饰器的返回值也是一个函数/类对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景,装饰器是解决这类问题的绝佳设计。有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码到装饰器中并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能

在Python中,函数是对象这很重要; Python本身也提供了一些装饰器:property,staticmethod等等

简单装饰器

def print_log(func):
    def wrapper():
        print("%s is running" % func.__name__)
        return func() # 把foo当做参数传递进来时,执行func()就相当于执行foo()
    return wrapper

def foo():
    print("I'm foo")

if __name__ == '__main__':
    handler = print_log(foo) # 因为装饰器 print_log(foo) 返回的是函数对象 wrapper,这条语句相当于  handler = wrapper
    handler()

foo is running
I'm foo

print_log 就是一个装饰器,它一个普通的函数,它把执行真正业务逻辑的函数 func 包裹在其中,看起来像 foo 被 print_log 装饰了一样,print_log 返回的也是一个函数,这个函数的名字叫 wrapper。在这个例子中,函数进入和退出时 ,被称为一个横切面,这种编程方式被称为面向切面的编程

@语法糖

def print_log(func):
    def wrapper():
        print("%s is running" % func.__name__)
        return func()
    return wrapper

@print_log
def foo():
    print("I'm foo")

if __name__ == '__main__':
    foo()

@ 符号是装饰器的语法糖,它放在函数开始定义的地方。如果我们有其他的类似函数,我们可以继续调用装饰器来修饰函数,而不用重复修改函数或者增加新的封装。这样,我们就提高了程序的可重复利用性,并增加了程序的可读性。

装饰器在 Python 使用如此方便都要归因于 Python 的函数能像普通的对象一样能作为参数传递给其他函数,可以被赋值给其他变量,可以作为返回值,可以被定义在另外一个函数内

带参数的被装饰函数

如果业务函数foo包含参数,不管是多个参数都可以传递下去

def print_log(func):
    def wrapper(*args,**kwargs):
        print("%s is running" % func.__name__)
        func(*args,**kwargs)
    return wrapper

@print_log
def foo(*args,**kwargs):
    print("I'm %s", args)

带参数的装饰器

装饰器还有更大的灵活性,例如带参数的装饰器,在上面的装饰器调用中,该装饰器接收唯一的参数(隐含参数)就是执行业务的函数foo 。装饰器的语法允许我们在调用时,提供其它参数,比如@decorator(a)。这样,就为装饰器的编写和使用提供了更大的灵活性。比如,我们可以在装饰器中指定日志的等级,因为不同业务函数可能需要的日志级别是不一样的。

def print_log(level):
    def decorator(func):
        def wrapper(*args,**kwargs):
            if level == 'warn':
                print("warn")
            elif level == 'error':
                print("error")
            func(*args,**kwargs)
        return wrapper
    return decorator

@print_log(level='warn')
def foo(*args,**kwargs):
    print("I'm %s", args)

if __name__ == '__main__':
    foo('anthonyzero')

 输出:   
 warn
 I'm %s ('anthonyzero',)

类装饰器

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

class Foo(object):
    def __init__(self, func):
        self._func = func

    def __call__(self, *args, **kwargs):
        print("before")
        self._func(*args, **kwargs)
        print("after")
@Foo
def bar(*args, **kwargs):
    print(args)
    print(kwargs)

if __name__ == '__main__':
    bar(1,2,3, a='a',b='b')

 before
 (1, 2, 3)
 {'a': 'a', 'b': 'b'}
 after

装饰函数的属性保持

使用装饰器极大地复用了代码,但是他有一个缺点就是原函数的元信息不见了,比如函数的docstring、name、参数列表

import functools

def warps_notuse(func):  # 返回目标函数结果的装饰器
    def warpper(*args, **kwargs):
        print(func.__name__)
        result = func()  # 这里就是真正的执行逻辑
        return result
    return warpper  # 返回包装后的函数

@warps_notuse
def f1():
    print("函数名为 %s" % f1.__name__)

def warps_use(func):  # 返回目标函数结果的装饰器
    @functools.wraps(func)
    def warpper(*args, **kwargs):
        print(func.__name__)
        result = func()  # 这里就是真正的执行逻辑
        return result
    return warpper  # 返回包装后的函数

@warps_use
def f2():
    print("函数名为 %s" % f2.__name__)

if __name__ == '__main__':
    f1()
    f2()
输出:
f1
函数名为 warpper
f2
函数名为 f2

如果wrapper函数没有使用@functools.wraps装饰的话,f1.name返回的是wrapper函数的名字,这是因为我们返回的函数已经是wrapper这个函数,所以name属性也就不是原来函数了的。

好在我们有functools.wraps,wraps本身也是一个装饰器,它能把原函数的元信息拷贝到装饰器里面的 func 函数中,这使得装饰器里面的 func 函数也有和原函数 foo 一样的元信息了。

总结

  • 装饰器语法糖 @ 就是拿把被装饰的函数作为参数传递到装饰器函数里面加工,最后执行被装饰函数的时候,就相当于执行了一个加工后的函数。
  • 装饰器的本质是函数,其参数是(被装饰的函数)。 装饰器通常会额外处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。行为良好的装饰器可以重用,以减少代码量.