Python 装饰器


前言:

内裤可以用来遮羞,但是到了冬天它没法为我们防风御寒,聪明的人们发明了长裤,有了长裤后宝宝再也不冷了,装饰器就像我们这里说的长裤,在不影响内裤作用的前提下,给我们的身子提供了保暖的功效。

一、什么是装饰器?

装饰器(decorator)是函数嵌套定义的另一个重要应用。装饰器本质上也是一个函数,只不过这个函数接收其他函数作为参数并对其进行一定的改造之后返回新函数。

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


二、需要的前置知识:

在此之前需要掌握函数带括号不带括号的时候分别表示什么:

现在有一个cal函数定义如下:

def cal(x, y):
    result = x + y
    return result

此时可以说,我们创建了一个叫做cal的函数对象,然后你可以这样使用它:

cal(1, 2)

或者,这样使用它:

calculate = cal
calculate(1, 2)

在第一种方式下,我们直接使用了cal这个函数对象;在第二种方式下,我们将一个名叫calculate的变量指向了cal这个函数对象,可以把这个过程看作“实例化”

也就是说,对象,就像是一个模子,当你需要的时候,就用它倒一个模型出来,每一个模型可以有自己不同的名字。在上面的例子中,calculate是一个模型,而cal函数就是一个模子。

OK,接下来就可以理解函数带括号和不带括号分别代表的意义了:

在上面那个例子中,如果只写一个cal,那么此时的cal仅代表一个函数对象,但是当我们写cal(1, 2)的时候,就相当于告诉编译器 “执行cal这个函数”


三、装饰器:

现在假设我们要编写一个函数,计算两个数相加所需要的时间:

import time


def add(a, b):
    start_time = time.time()
    res = a + b
    exec_time = time.time() - start_time
    print(f'add函数,花费的时间是:{exec_time}')
    return res


add(1, 2)

然而,假如现在又有减法、乘法、除法等各种函数,你都想计算他们的耗时,因此需要同样编写像上面一样长的代码很多次,显然这样会显得很麻烦,而且也不灵活,万一计算时间的代码有所改动(假设需要每个计算的时间都保留两位小数),每个函数都得改……

此时,我们的装饰器就是最好的解决方案:

import time


def timer(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        res = func(*args, **kwargs)
        exec_time = time.time() - start_time
        print(func.__name__, "函数花费的时间是:", exec_time)
        return res

    return wrapper


@timer
def add(a, b):
    return a + b


@timer
def sub(a, b):
    return a - b


add(1, 2)
sub(1, 2)

注:

在这个装饰器中,返回的 res 是被修饰函数 func 的返回值。也就是说,在使用 timer 装饰器修饰一个函数后,被修饰函数的返回值会被装饰器包装成一个新的函数,并再次返回到调用端。具体代码示例如下:

@timer
def my_func(x):
  time.sleep(x)
  return x

# 调用
result = my_func(2) # 输出 "my_func 函数花费的时间是: 2.002307176589966",返回值为 2

wrapper 这个新函数中,我们首先调用了被修饰函数 func,并将其返回值 res 存储下来。接着我们计算出 func 执行的时间,并将其输出到控制台。最后,我们使用 return 语句返回 res,是为了让被装饰函数的返回值能够被正常地传递到调用端,而不会被装饰器函数丢失。

当装饰器函数返回 wrapper 函数时,我们没有加上括号的原因是,此处返回的是一个函数对象,而不是调用函数得到的返回值。换句话说,我们返回的是一个可调用对象,而不是调用函数。因此,没有必要在返回语句中加上括号。同时,如果在返回时加上了括号,就相当于是在调用返回的 wrapper 函数,这应该不是我们想要的效果。


四. 装饰器运行的顺序

在函数定义阶段:执行顺序是从最靠近函数的装饰器开始,自内而外的执行
在函数执行阶段:执行顺序由外而内,一层层执行

def deco1(func):
  def wrapper():
    print('deco1 start')
    func()
    print('deco1 end')
  return wrapper

def deco2(func):
  def wrapper():
    print('deco2 start')
    func()
    print('deco2 end')
  return wrapper

@deco1
@deco2
def my_func():
  print('original function')

my_func()

该示例中,my_func 函数被 deco1deco2 装饰器修饰。在函数执行时,实际上等价于以下代码:

my_func = deco1(deco2(my_func))
my_func()

运行结果如下:

deco1 start
deco2 start
original function
deco2 end
deco1 end

很难用语言描述这个顺序,意会一下。我感觉是因为定义的时候返回的是函数对象,并没有执行,然后在执行的时候就是最外层最先开始执行。


参考文章:

如何理解Python装饰器(知乎)

一文带你迅速掌握python装饰器(微信公众号)

python装饰器函数执行顺序(CSDN)


  目录