Python基础笔记(2)-Python装饰器

装饰器

介绍

Python中的装饰器可以允许其他函数在不需要做任何代码修改的前提下额外增加功能,在函数执行的前后增加新的功能,类似于AOP,面向切面编程。

在Python中,函数是一等公民,函数可以作为参数、返回值等而存在。考虑如下的场景,我们希望在每个函数调用之前执行日志打印功能,输出当前函数的名称。我们可以通过在每个函数开头增加相关功能,但是过于繁琐也不便于同一管理。我们可以通过定义一个统一的debug函数,它的参数是一个函数func,返回值也是一个函数wrapper。作为返回值的wrapper函数,它调用了参数func,并且在调用前后执行了相关操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def debug(func):
def wrapper():
print("[DEBUG]: {}() start...".format(func.__name__))
res = func()
print("[DEBUG]: {}() finish...".format(func.__name__))

return res

return wrapper


def function1():
print("Hello I am Function1")
return 1


function1 = debug(function1) # 覆盖函数名达到相同的调用效果,添加功能并保持原函数名不变
res = function1()
print(res)

上面的函数的执行结果如下:

1
2
3
4
[DEBUG]: function1() start...
Hello I am Function1
[DEBUG]: function1() finish...
1

上面的操作实际上就是装饰器完成的工作。Python中的装饰器本质上还是一个函数,它的返回值也是一个函数。不过这里我们每次还需要执行function1 = debug(function1)来保持原函数名不变,而Python给我们提供了@语法糖,我们可以使用下面的形式:

1
2
3
4
5
6
7
8
@debug
def function1():
print("Hello I am Function1")
return 1


res = function1()
print(res)

这里的@是装饰器的语法糖,在定义函数的时候使用,可以避免再一次赋值操作,相当于帮助我们执行了function1 = debug(function1),因此上面代码也能得到相同的输出,实现了函数功能的增强。

带参数的装饰器

在实际的场景中,很多函数都是需要带有参数的。为了实现这种装饰器,我们可以指定装饰器函数wrapper接受和原函数一样的参数,也可以使用可变参数*args和关键字参数**kwargs来匹配任何的函数参数,作用于任意目标函数,提高通用性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def debug(func):
def wrapper(*args, **kwargs):
print("[DEBUG]: {}() start...".format(func.__name__))
res = func(*args, **kwargs)
print("[DEBUG]: {}() finish...".format(func.__name__))

return res

return wrapper


@debug
def say(something):
print("I say: {}".format(something))


say("hello")

输出如下:

1
2
3
[DEBUG]: say() start...
I say: hello
[DEBUG]: say() finish...

上面的参数是被装饰函数的参数,实际上装饰器函数也能有自己的参数。我们可以增加一层嵌套函数来接收装饰器函数的参数,下面实现的装饰器接收参数times,表示重复调用函数func的次数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def debug(times):
def outerWrapper(func):
def wrapper(*args, **kwargs):
print("[DEBUG]: {}() start...".format(func.__name__))
for i in range(times):
func(*args, **kwargs)
print("[DEBUG]: {}() finish...".format(func.__name__))

return wrapper

return outerWrapper


@debug(times=3)
def say(something):
print("I say: {}".format(something))


say("hello")

输出如下:

1
2
3
4
5
[DEBUG]: say() start...
I say: hello
I say: hello
I say: hello
[DEBUG]: say() finish...

多层装饰器的执行顺序:从上到下依次执行,先执行的后退出,后执行的先退出。

@functools.wrap

如果我们直接利用上面的代码,执行print(say),得到的输出结果如下:

1
<function debug.<locals>.outerWrapper.<locals>.wrapper at 0x000001CA442A05E0>

可以发现我们原本的say函数信息被装饰器取代了,函数名等函数属性发生了改变。这对结果执行不会产生影响,但是如果在一些场景下,我们需要使用到函数的元信息,那么应该需要能够保留。这可以使用内置装饰器@functools.wrap来实现,它能够帮助我们保留原有函数的名称和函数属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import functools


def debug(times):
def outerWrapper(func):
@functools.wraps(func) # 保留元信息
def wrapper(*args, **kwargs):
print("[DEBUG]: {}() start...".format(func.__name__))
for i in range(times):
func(*args, **kwargs)
print("[DEBUG]: {}() finish...".format(func.__name__))

return wrapper

return outerWrapper


@debug(times=3)
def say(something):
print("I say: {}".format(something))


print(say)

此时再执行print(say),得到的结果就是没有被取代的元信息:

1
<function say at 0x00000237509505E0>

类装饰器

除了利用函数来完成装饰器,我们也可以通过类来实现一个装饰器。类能够实现装饰器的功能,是因为我们可以调用类的实例,实际上就调用到了__call__()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class DEBUG:
def __init__(self, func):
self.func = func

def __call__(self, *args, **kwargs):
print("[DEBUG]: {}() start...".format(self.func.__name__))
self.func(*args, **kwargs)
print("[DEBUG]: {}() finish...".format(self.func.__name__))


@DEBUG
def say(something):
print("I say: {}".format(something))


say("Hello")

如果装饰器需要参数的话,可以通过下面的方法来实现,在__call__()方法中定义内部函数,这里@functools.wraps的用法和之前也是类似的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class DEBUG:
def __init__(self, times):
self.times = times

def __call__(self, func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("[DEBUG]: {}() start...".format(func.__name__))
for i in range(self.times):
func(*args, **kwargs)
print("[DEBUG]: {}() finish...".format(func.__name__))

return wrapper


@DEBUG(times=3)
def say(something):
print("I say: {}".format(something))

参考文章

  1. Python装饰器,读完这篇你就懂了 - 掘金 (juejin.cn)
  2. 详解Python的装饰器 - Toby Qin - 博客园 (cnblogs.com)

Python基础笔记(2)-Python装饰器
http://example.com/2022/10/26/Python基础笔记-2-Python装饰器/
作者
EverNorif
发布于
2022年10月26日
许可协议