Python装饰器详解

本篇文章讨论Python的装饰器,Python装饰器帮助我们在不改变函数内部结构的情况下,对函数功能进行扩展,接下来进入正题,在讨论装饰器之前,先拿函数定义的原理抛砖引玉

Python version: 3.5+

函数定义的原理

在Python中通过def fun_name(): pass来定义一个函数。那么在这个小函数中,fun_name是函数的名字,pass是函数体。我们通常会说,Python在解释代码时,会把pass函数体放在内存中,并在内存中用fun_name去指向pass函数体。

话是这么说没错,但对于初学者来说,同样都是放在内存中,为啥还要指来指去的呢?具体来说,函数名在内存存放的地区是栈区,函数体在内存中存放的地区是堆区,存在栈区的函数名会指向存在堆区的函数体。在前面的Python学习笔记中,提到的两大数据类型的分类,一个是基本数据类型,一个是引用数据类型,所有的引用数据类型的名字都是压在了栈区,其对应的对象,都是存在了堆区。所以关于指向的问题,并不在内存中胡乱去指的~~

装饰器

装饰器前传

现有如下程序猿代码:

1
2
3
4
5
6
7
8
def f1():
print('f1 func')

def f2():
print('f2 func')

def f3():
print('f3 func')

现在老板需求是:在打印输出前,先输出loading...

于是程序猿做出如下修改:

1
2
3
4
5
6
7
8
9
10
11
def f1():
print('loading...')
print('f1 func')

def f2():
print('loading...')
print('f2 func')

def f3():
print('loading...')
print('f3 func')

老板觉得,打印输入后输出Done更好

于是程序猿又去修改代码了~~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def f1():
print('loading...')
print('f1 func')
print('Done')

def f2():
print('loading...')
print('f2 func')
print('Done')

def f3():
print('loading...')
print('f3 func')
print('Done')

之后老板又觉得……


有了装饰器后

还是上面的代码:

1
2
3
4
5
6
7
8
def f1():
print('f1 func')

def f2():
print('f2 func')

def f3():
print('f3 func')

如果使用了装饰器实现loading的需求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def outer(func):
def inner():
print('loading...')
func()
return inner

@outer
def f1():
print('f1 func')

@outer
def f2():
print('f2 func')

@outer
def f3():
print('f3 func')

使用装饰器实现Done的需求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def outer(func):
def inner():
print('loading...')
func()
print('Done')
return inner

@outer
def f1():
print('f1 func')

@outer
def f2():
print('f2 func')

@outer
def f3():
print('f3 func')

从上面简单的例子可以看出,使用装饰器可以在不改变原函数的情况下,对原函数的功能进行扩展!

装饰器预热

在详解装饰器前,我们还需要强调一个很重要的概念,就是函数的调用!

函数怎么调用?函数名后面跟了小括号就是调用,不加括号就不调用!

重要的事情说三遍!!!

  1. 函数名后面跟了小括号就是调用,不加括号就不调用!
  2. 函数名后面跟了小括号就是调用,不加括号就不调用!
  3. 函数名后面跟了小括号就是调用,不加括号就不调用!
1
2
3
4
5
6
7
def func():
print('我被调用了')

func()

------------
我被调用了
1
2
3
4
5
6
def func():
print('我被调用了')

func

------------

下面的小程序只写了函数名,后没有跟括号,该函数被加载到内存,但是没有被调用!在学习装饰器之前,一定要清楚什么时候加括号什么时候不加括号

装饰器解析

装饰器的创建

创建装饰器,最少需要嵌套一层函数,且在内层函数加载到内存之后返回内存函数

装饰器的使用

使用装饰器的方法很简单,只需要在被装饰的函数上面用@符号引用装饰器名即可

装饰器的功能

装饰器有两大功能:

  • 自动执行装饰器函数,且将下面的函数名当做参数传递给装饰器函数
  • 将装饰器函数的返回值,重新赋值给函数

第一大功能,当Python在遇到@装饰器名时,会去执行该装饰器;我们来说说第二大功能,装饰器函数的返回值会重新赋值给函数!

所有装饰器函数的返回值都是其内层函数,也就是说,内层函数会重新赋值给原函数。说到这里时,就需要引入开篇提到的函数定义原理,下面我们假设程序调用了f1().我们来看看在Python的内部都发生了什么。

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
def outer(func):
def inner():
print('loading...')
func()
print('Done')
return inner

@outer
def f1():
print('f1 func')

@outer
def f2():
print('f2 func')

@outer
def f3():
print('f3 func')

f1()

------------
loading...
f1 func
Done
  1. 在该程序中,f1的函数被outer装饰器所装饰
  2. Python在从上至下解释代码时,先将outer装饰器加载到内存中
  3. 当执行到@outer时,触发了Python装饰器的第一大功能,他将帮助我们去执行装饰器函数,上一条,outer装饰器已经加载到内存,还没有被调用执行。遇到@outer后,会触发outer()操作,当outer函数被执行后,其内层函数将被加载到内存中等待调用,outer的最后,返回了内层inner函数
  4. 装饰器外层函数的调用返回内层函数后,触发了Python装饰器的第二大功能,他将自动帮我们把返回的内存的函数重新赋值给被装饰的函数f1
  5. 最后,当f1被调用时,实际指向的是inner函数的函数体,也就是说,当f1函数被装饰器所装饰后,调用时所运行的函数体,实际上是装饰器函数中的内层函数。

如果上面的文字描述你还没有看懂的话,没关系,下面还有图文并茂的~

  • Python解释器从上至下解释,先将outer这个函数名字放入栈区,outer的整个函数体将被完整的加载到内存的堆区中,内存状态如下:

  • 接下来,Python解释器遇到@outer时,Python内部控制将去自动将下一行函数的名字(f1)作为参数传入到outer装饰器函数中,并自动执行outer函数,此时outer函数的形式参数func在栈区指向了堆区的f1函数体,内存状态如下:

  • outer函数执行完后,会将内层函数的返回值重新复制给原函数(f1)想当于执行了f1 = outer(f1)

  • 最后,当f1被被调用时,可以通过上图看出,实际调用的是装饰的内层函数,而装饰器中的内存函数体里又引用了原f1的函数体(已经将f1函数体堆区地址引用给了栈区的func)

总结:通过上面剖析的小例子可以看出,虽然print('f1 func')是f1的函数体,但是f1只有一瞬间拥有该函数体。在遇到@outer之后,f1就被当做参数传递给了outer装饰器,func与f1同时指向放在堆区的函数体。在outer函数执行后,内层函数被重新赋值给了f1,此时f1失去了对原函数体的控制而指向了inner的堆区地址,func独自控制了print('f1 func')该函数体。


原函数有返回值的装饰器

现在我的基础函数有返回值的需求,改成了如下代码:

1
2
3
4
5
6
7
8
9
10
11
def f1():
print('f1 func')
return True

def f2():
print('f2 func')
return True

def f3():
print('f3 func')
return True

如果是这样的函数被装饰,函数体中不仅有计算和输出,而且有返回值,那么我们就需要在装饰器中,接收原函数的返回值,并进行返回:

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
27
28
29
30
31
32
def outer(func):
def inner():
print('loading...')
ret = func() # 因为原函数有返回值,在这里调用时去接收原函数的返回值
print('Done')
return ret # 在新函数的结尾,返回原函数的返回值
return inner


@outer
def f1():
print('f1 func')
return True


@outer
def f2():
print('f2 func')
return True


@outer
def f3():
print('f3 func')
return True

f1()

------------
loading...
f1 func
Done

带参数的函数被装饰(参数个数确定)

现在我的代码变成了带参数的函数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
def f1(arg1):
print('f1 func')
return True


def f2(arg1):
print('f2 func')
return True


def f3(arg1):
print('f3 func')
return True

相应的,我的装饰器也应该做出改变。想想上面装饰器的原理—>装饰器的内层函数被返回并重新赋值给原函数 也就是说,如果原函数有参数的话,相对应的,装饰器中的内层函数也应该有参数,故做出如下修改:

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
27
28
29
30
31
32
def outer(func):
def inner(arg):
print('loading...')
ret = func(arg)
print('Done')
return ret
return inner


@outer
def f1(arg1):
print('f1 func' + arg1)
return True


@outer
def f2(arg1):
print('f2 func' + arg1)
return True


@outer
def f3(arg1):
print('f3 func' + arg1)
return True

f1('123')

------------
loading...
f1 func123
Done

带参数的函数被装饰(参数个数不确定)

上面的例子中,在装饰器的内层函数加入参数,解决了被装饰函数带参数的问题,那么,如果被装饰的函数如果参数不确定,如何保证装饰器仍然可用呢?参考如下代码:

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
27
28
29
30
31
32
def outer(func):
def inner(*args, **kwargs):
print('loading...')
ret = func(*args, **kwargs)
print('Done')
return ret
return inner


@outer
def f1(arg1):
print('f1 func' + arg1)
return True


@outer
def f2(arg1, arg2):
print('f2 func' + arg1 + arg2)
return True


@outer
def f3(arg1, arg2, arg3):
print('f3 func' + arg1 + arg2 + arg3)
return True

f1('123')

------------
loading...
f1 func123
Done

好的,至此Python装饰器的常用用法就已经介绍完了,以上装饰器的知识足够解决Pythoner 80%的装饰问题啦,如果你有更高更复杂的需求,可以参考以下装饰器的用法


带参数的装饰器

应用场景:上面的装饰器,前篇一律,类似于Java中的工厂方法模式,各类对象,不管你是方的圆的三角的,一进工厂,出来都是一个模样的~~ 那带参数的装饰器要解决的就是“个性化工厂方法”的功能。虽然都会进装饰器的“熔炉”,但是通过给装饰器传递的不同参数,可以实现对每个函数的个性化装饰

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
27
28
29
30
31
32
33
34
35
def xxx(arg):
def outer(func):
def inner(*args, **kwargs):
print('loading...%s' % (arg))
ret = func(*args, **kwargs)
print('Done')
return ret
return inner
return outer


@xxx('f1f1f1')
def f1(arg1):
print('f1 func'+ arg1)
return True


@xxx('f2f2f2')
def f2(arg1, arg2):
print('f2 func' + arg1 + arg2)
return True


@xxx('f3f3f3')
def f3(arg1, arg2, arg3):
print('f3 func' + arg1 + arg2 + arg3)
return True


f1('123')

------------
loading...f1f1f1
f1 func123
Done

我们使用调试模式来辅助理解带参数的装饰器是怎么执行的

Python解释器加载xxx函数到内存中

执行了xxx函数,并将参数传递给了xxx 相当于执行了xxx('f1f1f1')

解释器走到xxx函数的内部,开始执行xxx的函数体,发现又是一个函数,于是将代码完整的载入到内存中以备调用

xxx执行完毕后,把加载到内存中的outer返回

outer返回后被立即执行(前面提到过,装饰器的两个功能,第一大功能就拿到下一行的函数名当做参数传递给装饰器。在带参的装饰器中,调用装饰器时,其自身已经带有参数,不会直接去找下一行的函数进行传参操作,而是会先用自己已有的参数去执行自己,返回outer后,outer发现自己需要参数,于是去下一行找函数名并传参,接下来发生的事情就和上面介绍的不带参的装饰器的流程是一样的了)

outer函数被执行,将inner函数体加载到内存中

将inner返回(这是这里是将inner函数返回,不要在inner后面加括号!加了括号表示把inner执行结果的返回值返回给原函数f1)

装饰器内的操作执行完后,跳回到调用装饰器的地方(因为Python内部已经将原f1函数体的引用通过传参的方式指给了outer函数的func形参,所以Python解释器不会再去读f1的函数体了)

跳过f1的函数体之后进入到对下一个函数的装饰 …

最后Python解释器读到了程序的入口点(终于可以干活了!)

look!直接跳到了inner函数的内部去执行啦!看来之前分析的没有错,装饰器在返回的时候,将inner函数重新赋值给了f1

在运行到inner中的func(*args, **kwargs)的时候,程序跳到了f1原有的函数体。这也证明了上面的分析,func成为了唯一指向原函数体的变量


参数为函数的装饰器

这样的需求也是有的,看过了上面的个性化定制之后,现在又有了新的需求,就是装饰器有可能会经常变,老板说了,时间长了会审美疲劳~~

应对这样的需求,装饰器也是可以做的,那就是让装饰器传递一个函数,该函数代替了之前写死在装饰器里的代码,可以灵活改变,而且更厉害的是,被装饰器当做参数的函数依然可以有参数~~

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def before(arg):
print('before' + arg)


def after(arg):
print('after' + arg)


def xxx(func_before, func_after):
def outer(func):
def inner(arg):
func_before(arg)
ret = func(arg)
func_after(arg)
return ret
return inner
return outer


@xxx(before, after)
def f1(arg1):
print('f1 func'+ arg1)
return True


@xxx(before, after)
def f2(arg1, arg2):
print('f2 func' + arg1 + arg2)
return True


@xxx(before, after)
def f3(arg1, arg2, arg3):
print('f3 func' + arg1 + arg2 + arg3)
return True


f1('123')

------
before123
f1 func123
after123

通过上面这种方法实现后,在原有函数的基础上,可以随时方便的更改装饰内容~~


多层装饰器

明白了单层装饰,多层装饰器就很容易理解啦~~

现对业务的具体功能有如下需求:

  • 首先验证用户是否登录
  • 再验证登录的用户是否拥有权限

现在我有大量的业务逻辑方法,每个方法在被调用前都需要去验证前两项,在这样一个项目需求中,多层装饰器就可以被发挥的淋漓尽致

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
27
28
29
30
31
32
33
login_status = True
permission = True


def check_login(func):
def inner():
if login_status:
print('--->Login success')
return func()
return inner


def check_permission(func):
def inner():
if permission:
print('--->Authorized')
return func()
return inner


@check_login
@check_permission
def xxx():
print('rm -fr /')


if __name__ == '__main__':
xxx()

------------
--->Login success
--->Authorized
rm -fr /

从上面的代码中可以看出,在一个函数使用了多个装饰器之后,代码是从第一个装饰器的内部函数(inner)开始顺序向下执行,所以在使用多个装饰器时,一定要注意顺序问题!一定要在最上面先验证登录,再验证权限!接下来我们来具体分析一下,为什么不是离被装饰函数最近的装饰器先执行!

如果使用Debug模式来观察执行过程,可以很清楚的看到如下过程:

  1. 首先Python解释器将代码的前两行放入到内存中
  2. 遇到check_login函数,将其函数名放入栈区,函数体放入堆区
  3. 遇到check_permission函数,将其函数名放入栈区,函数体放入堆区
  4. 执行到@check_login时,将获取下一行的函数作为参数,但是下一行不是函数,所以继续向下执行
  5. 执行到@check_permission时,获取下一行函数作为参数,并返回check_permission装饰器内的inner函数返回重新赋值给xxx
  6. 此时之前跳过的@check_login装饰器终于得到了一个新函数作为参数(@check_login获得的函数是@check_permission内部的inner函数)故,将该函数传入到login装饰器中,并返回@check_login内部的inner函数。
  7. 当xxx函数被执行的时候,先执行的是@check_login中返回的inner函数,而在@check_login中的inner函数遇到fun()时,其实该函数是@check_permission函数返回的inner函数,所以在验证完登录后,会立即去permission函数中去验证权限,权限验证通过后再执行的fun()就是xxx函数自己原始的函数体了

总结:顺序很重要!多重嵌套的装饰一定要记住以下两点:

  • Python解释代码时是从下往上返回函数
  • 而程序执行时,功能是从上往下执行