函数装饰器


# 函数装饰器

函数装饰器(Function Decorators):从字面上理解,就是装饰一个函数。可以在不修改原代码的情况下,为被装饰的函数添加一些功能并返回它。

函数装饰器的语法是将 @装饰器名 放在被装饰函数上面,下面是个例子:

@dec
def func():
    pass
1
2
3

# 前置概念

首先需要明确以下几个概念和原则,才能更好的理解装饰器:

  • Python 程序是从上往下顺序执行的,碰到函数的定义代码块不会立即执行,只有等到该函数被调用时,才会执行其内部的代码块。
  • Python 中函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数。
  • 可以将一个函数作为参数传递给另一个函数。

有了这些基本的概念,我们就可以通过一个实例来讲解 Python 中函数装饰器的用法了。

# 模拟场景

模拟一个场景,假设在某项目中,有下列五个接口(f1~f5):

def f1():
    print("第一个接口......")
def f2():
    print("第二个接口......")
def f3():
    print("第三个接口......")
def f4():
    print("第四个接口......")
def f5():
    print("第五个接口......")
1
2
3
4
5
6
7
8
9
10

现在有个需求,每个接口执行完后需要记录日志。如果逐次修改这五个接口的内部代码显然是一种比较糟糕的方案,我们可以使用装饰器完成这一任务。代码如下:

def outer(func):
    def inner():
        result = func()
        print("日志添加成功")
        return result
    return inner

@outer
def f1():
    print("第一个接口......")

@outer
def f2():
    print("第二个接口......")

@outer
def f3():
    print("第三个接口......")

@outer
def f4():
    print("第四个接口......")

@outer
def f5():
    print("第五个接口......")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

使用装饰器 @outer,仅需对原接口代码进行拓展,就可以实现操作结束后保存日志,并且无需对原接口代码做任何修改,调用方式也不用变。

# 实现原理

下面以 f1 函数为例,对函数装饰器的实现原理进行分析:

def outer(func):
    def inner():
        result = func()
        print("日志添加成功")
        return result
    return inner

@outer
def f1():
    print("第一个接口......")
1
2
3
4
5
6
7
8
9
10
  • Step1:程序开始运行,解释器从上往下逐行解释并运行代码,读到 def outer(func): 的时候,把函数体加载到内存里,然后过。

  • Step2:读到 @outer 的时候,程序发现这是个装饰器,按规则要立即执行它,于是程序开始运行 @ 后面那个名为 outer 的函数。(@outer 相当于 outer(f1)

  • Step3:程序正式进入到 outer 函数,开始执行装饰器的语法规则。规则是:

    • 被装饰的函数(整个函数体)会被当作参数传递给装饰函数。
    • 装饰函数执行它自己内部的代码后,会将它的返回值赋值给被装饰的函数
    • 此处:原来的 f1 函数被当做参数(func)传递给了 outer,而 f1 这个函数名之后会指向 inner 函数。

(装饰前后函数名 f1 指向)

  • Step4:程序开始执行 outer 函数内部的内容,一开始它又碰到了一个函数 innerinner 函数定义块被程序观察到后不会立刻执行,而是读入内存中(这是默认规则)。

  • Step5:再往下,碰到 return inner,返回值是个函数名,并且这个函数名会被赋值给 f1 这个被装饰的函数,也就是 f1 = inner换句话说:此时 f1 函数被新的函数 inner 覆盖了

  • Step6:接下来,当调用方依然通过 f1() 的方式调用 f1 函数时,执行的就不再是旧的 f1 函数的代码,而是 inner 函数的代码。

    • 在本例中,它会先执行 func 函数并将返回值赋值给变量 result,这个 func 函数就是旧的 f1 函数;接着,它会打印日志保存,这只是个示例,可以换成任何你想要的;最后返回 result 这个变量给调用方。
  • Step7:最后,调用方可以和以前一样通过 res = f1() 的方式接受 result 的值。

# 为什么要两层函数

这里可能会有疑问,为什么我们要搞一个 outer 函数一个 inner 函数这么复杂呢?一层函数不行吗?

答:因为 @outer 这句代码在程序执行到这里的时候就会自动执行 outer 函数内部的代码,如果不封装一下,在调用方还未进行调用的时候,就执行了,这和初衷不符。当然,如果你对这个有需求也不是不行。

请看下面的例子,它只有一层函数:

def outer(func):
    result = func()
    print("日志添加成功")
    return result

@outer
def f1():
    print("第一个接口......")
1
2
3
4
5
6
7
8

尝试敲一下上述代码,可以发现我们只是定义好了装饰器,还没有调用 f1 函数呢,程序就把工作全做了。这就是为什么要封装一层函数的原因。

# 函数参数传递

上面的例子中,f1 函数没有参数,在实际情况中肯定会需要参数的,函数的参数怎么传递的呢?看下面一个例子:

def outer(func):
    def inner(username):
        result = func(username)
        print("日志添加成功")
        return result
    return inner

@outer
def f1(name):
    print("{0}正在连接第一个接口......".format(name))

# 调用方法
f1("zhangsan")
1
2
3
4
5
6
7
8
9
10
11
12
13

inner 函数的定义部分也加上一个参数,调用 func 函数(即装饰后的 f1 函数)时传递这个参数就可以了。、

可问题又来了,如果 f2 函数有 2 个参数,f3 函数有 3 个参数,该怎么传递?通过万能参数 *args**kwargs 就可以了。简单修改一下上面的代码:

def outer(func):
    def inner(*args, **kwargs):
        result = func(*args,**kwargs)
        print("日志添加成功")
        return result
    return inner

@outer
def f2(name, age):
    print("{0}正在连接第二个接口......".format(name))

# 调用方法
f2("lisi", 14)
1
2
3
4
5
6
7
8
9
10
11
12
13

# 多层装饰器

上面已经介绍了函数装饰器的基本概念和用法,接下来再进一步,一个函数可以被多个函数装饰吗?答案是可以的。看下面的例子:

def outer1(func):
    def inner(*args, **kwargs):
        print("身份认证成功")
        result = func(*args, **kwargs)
        print("日志添加成功")
        return result
    return inner

def outer2(func):
    def inner(*args, **kwargs):
        print("代码开始执行")
        result = func(*args, **kwargs)
        print("代码执行完毕")
        return result
    return inner

@outer1
@outer2
def f1(name, age):
    print("{0}正在连接第一个接口......".format(name))

# 调用方法
f1("zhangsan", 13)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

怎么分析多装饰器情况下的代码运行顺序呢?可以将它理解成洋葱模型:每个装饰器一层层包裹住最内部核心的原始函数,执行的时候逐层穿透进入最核心内部,执行内部核心函数后,再反向逐层穿回来。

所以,最后的运行结果就显而易见了:

身份认证成功
代码开始执行
zhangsan正在连接第一个接口......
代码执行完毕
日志添加成功
1
2
3
4
5

# 装饰器携带参数

装饰器自己可以有参数吗?答案也是可以的。看下面的例子:

def say_hello(country):
    def wrapper(func):
        def deco(*args, **kwargs):
            if country == "China":
                print("你好")
            elif country == "America":
                print("Hello")
            else:
                return
            func(*args, **kwargs)
        return deco
    return wrapper

@say_hello("China")
def f1():
    print("我来自中国")

@say_hello("America")
def f2():
    print('I am from America')

f1()
f2()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

运行结果:

你好
我来自中国
Hello
I am from America
1
2
3
4

# 总结

装饰器体现的是设计模式中的装饰模式,实际上,在 Python 中装饰器可以用函数实现,也可以用类实现。我在实际开发中函数装饰器用的比较多,所以本文主要介绍的是函数装饰器。

而如果要对装饰器的用法作更加深入的学习,官方文档和框架源码是比较好的学习对象。

(完)