我没有学过太多语言,所以python的装饰器是我见过的最灵活的。—— 松言松语

装饰器

装饰器的前提

顾名思义,装饰类就是用一个装饰函数a来修饰另外一个函数b,使得函数b的功能得到增强。

在java(编译型语言)中,增强一个函数可以通过AOP实现,有两种方式:

1. 编译期,是通过修改字节码实现,如AspectJ;
2. 运行期,是通过生成一个代理对象实现的,如Spring AOP;

python是通过闭包来做的。

闭包:

一个函数(就叫它外函数吧)里面定义了一个函数(称之为内函数),内函数中用使用到了外函数中的变量,并且,外函数直接返回了这个内函数的引用

简单来说,就是 函数套函数,函数返回函数 ,举个例子

def f1(x, y):
    result = x + y

    def f2():
        print(result)
        return result
    return f2


y = f1(1, 2)
y()
# output : 

可以看到f1中“套了”一个f2,f2用到了f1中的变量,f1返回了f2。这就是闭包。理解这个,下面再来看一下python的装饰器。

装饰器函数

from functools import wraps


def decorator(func):
    """定义一个装饰器函数"""
    @wraps(func)
    def decoration_function(*args, **kwargs):
        print("start...")
        func(*args, **kwargs)
        print("end...")
    return decoration_function;


@decorator
def test():
    """被装饰的方法"""
    print("running")


test()
  • 一共有三个函数,一个是被装饰的函数(test),外函数(decorator)、内函数(decoration_function);

  • 内函数用@wraps(被修饰函数)来修饰。其中,func就是被修饰的函数的变量表示;

  • 被修饰的函数用@外函数修饰如,@decorator;

  • 被装饰的函数(test)作为外函数(decorator)的一个参数(func),传给内函数(decoration_function)并在内函数中执行;

  • 外函数(decorator)返回内函数(decoration_function)的引用;

总结一下步骤,就是3-2法则:

  • 3函数。函数套函数,外函数返回内函数;

  • 2注解。@wraps(被修饰函数)@外函数;

需要提醒一下的是,

  1. @decorator 是一个语法糖,只是为了简化操作,从而看起来更像一个装饰器
@decorator
def test():
    """被装饰的方法"""
    print("running")
    
test()  

等价于

def test():
    """被装饰的方法"""
    print("running")

y = decorator(test)
y()

实际上,就是把被装饰函数test作为参数传到装饰器里面去。

  1. @wraps的作用,是为了维持被装饰函数(test)执行的时候,使其函数性质保持不变。

    正因为 @decorator是语法糖,所以执行的时候可能存在一些问题。

    def decorator(func):
        """定义一个装饰器函数"""
    
        def decoration_function(*args, **kwargs):
            print("start...")
            func(*args, **kwargs)
            print("end...")
        return decoration_function
    
    
    @decorator
    def test():
        """被装饰的方法"""
        print("running")
    
    print(test.__name__)
    # output : decoration_function
    
    

    去掉@wraps(func) 执行,你会发现 被装饰函数 test 其实就是闭包里返回的内函数decoration_function嘛(其实这也反向证明了 @decorator是语法糖),但我们期望的输出应该是test,这不科学!

    test的名字、属性等都变了(虽然执行结果一样),这还能叫装饰器吗?叫做代理可能会更合理一些。

    所以python中提供了@wraps(被修饰函数),是为了声明这是一个修饰器的内函数,并复制了被修饰函数原有的名字、属性等。

装饰器类

装饰器还可以以class的形式构建。

如下所示

from functools import wraps
import time


class TimeWrapper: # 计算一个方法的执行时间 
    def __init__(self, name):
        self.name = name

    def __call__(self, func):
    """
        提供__call__的原因,是把TimeWrapper变成一个可直接执行的类;
        调用类的时候,会触发__call__中的方法,参数也是通过__call__传进来;
    """
            @wraps(func)
            def wrapped_function(*args, **kwargs):
                print('start....')
                start_time = time.perf_counter()
                result = func(*args, **kwargs)
                print('end....')
                print(f'运行时间:{time.perf_counter() - start_time:8f}s')
                return result
            return wrapped_function


# 通过注解的形式来调用装饰器类TimeWrapper,注意,此时,默认调用的是类中的__call__
@TimeWrapper('looping')
def loop(n):
    for i in range(1, n):
        print(i)


loop(4)

要点是:

  • 要在类中定义一个__call__函数,以确保类是可执行的,调用的时候,自动执行__call__函数;
  • __call__作为外函数存在;
  • 使用修饰器的时候,用@类名