理解Python中的Decorator

前言

之前看廖雪峰老师的 Python 教程的时候卡在装饰器那一节,干脆跳了过去。

现在看到@property的使用,又涉及到了装饰器的相关知识,于是只好回头来好好学一下了。。。

在了解装饰器之前,建议先了解一下什么是闭包

什么是装饰器

定义

装饰器本质上是一个 Python 函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。

它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。

概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。

基础装饰器

这里使用两个实际例子来说明一下最基础的装饰器

日志打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#Decorator
def logger(func):
def wrapper(*args, **kw):
print('Start function: {} '.format(func.__name__))
func(*args, **kw) #The function
print('End')
return wrapper
@logger
def add(x, y):
print('{} + {} = {}'.format(x, y, x+y))

add(2,3)

#outupt:

#Start function: add
#2 + 3 = 5
#End

计时器

计算一个函数的执行时长

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#Decorator
import time
def timer(func):
def wrapper(*args, **kw):
start = time.time()
time.sleep(1) # Time spent is too short without a sleep
func(*args, **kw)
end = time.time()
cost_time = end - start
print("Time spent:{}".format(cost_time))
return wrapper
@timer
def add(x, y):
print('{} + {} = {}'.format(x, y, x+y))
add(2,3)

#output:

#2 + 3 = 5
#Time spent:1.000917673110962

拿计时器这个例子来说

@timer放到了add函数的定义,它就像一顶帽子一样戴在这个函数的头上。和这个函数绑定在一起。在我们调用这个函数的时候,第一件事并不是执行这个函数,而是将这个函数做为参数传入它头顶上这顶帽子,这顶帽子我们称之为装饰函数装饰器

其实我们做到了这样一个实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import time
def timer(func):
def wrapper(*args, **kw):
start = time.time()
time.sleep(1) # Time spent is too short without a sleep
func(*args, **kw)
end = time.time()
cost_time = end - start
print("Time spent:{}".format(cost_time))
return wrapper

def add(x, y):
print('{} + {} = {}'.format(x, y, x+y))

timer(add)(2,3)
#output:

#2 + 3 = 5
#Time spent:1.000917673110962

#一般而言,函数后面只有一个括号。如果看见括号后还有一个括号,说明第一个函数返回了一个函数,如果后面还有括号,说明前面那个也返回了一个函数。以此类推。

timer(add)返回了一个wrapper (*args, **kw) 函数,所以又加了一个(2,3)传入wrapper内供func()调用,这里的func()其实也就是add()

带参数的装饰器

如果要让装饰器带参数,那么需要两层的嵌套

打招呼

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
#Parametric decorator

def say_hello(country):
def wrapper(func):
def deco(*args, **kwargs):
if country == "china":
print("你好!")
elif country == "america":
print('hello.')
func(*args, **kwargs)
return deco
return wrapper

@say_hello("china")
def american(name):
print("我来自中国,我叫" + name)

@say_hello("america")
def chinese(name):
print("I am from America, and my name is " + name)

american('张三')
chinese('Alan')
#output:

#你好!
# 我来自中国,我叫张三
# hello.
# I am from America, and my name is Alan

更高级的日志打印

我们现在要给每条日志加上level ,就像这样

1
2
[DEBUG] Log1
[WARN] Log2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#Parametric decorator
def logger(level):
def wrapper(func):
def inner_wrapper(*args, **kw):
print(f'[{level}] Start function {func.__name__}')
func(*args, **kw)
print(f'[{level}] End function {func.__name__}')
return inner_wrapper
return wrapper

@logger(level='DEBUG')
def add(x, y):
print('{} + {} = {}'.format(x, y, x+y))

add(2,3)

#output:
#[DEBUG] Start function add
#2 + 3 = 5
#[DEBUG] End function add

@functools.wraps()

在刚才的计时器例子中,如果你留心注意

1
2
>>> add.__name__
'wrapper'

举个例子来看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#Decorator
import time
def timer(func):
def wrapper(*args, **kw):
start = time.time()
func(*args, **kw)
end = time.time()
cost_time = end - start
print("Func:{1} Time spent:{0}".format(cost_time, func.__name__))
return wrapper
@timer
def add(x, y):
print('{} + {} = {}'.format(x, y, x+y))
print(add.__name__) #在add函数体中打印函数名
add(2,3)

#output:

#2 + 3 = 5
#wrapper
#Func:add Time spent:0.0

这里我们打印了两次函数名,但是却出现了不同的结果。为什么?

我们使用装饰器后,

1
add = timer(add)

timer函数返回的就是wrapper, 这样子的话其实就是

1
add = wrapper

那么add的 name 自然就是wrapper的 name,所以出现了第二行的输出结果

我们执行add(2,3)的时候,add函数被作为func传入timer函数中,所以func.__name__ 就是add.__name__ ,所以输出的第三行就是add

不仅仅是__name__, 其他的属性也会随之改变。

如何避免? 使用functools.wraps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#Decorator
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kw):
start = time.time()
func(*args, **kw)
end = time.time()
cost_time = end - start
print("Func:{1} Time spent:{0}".format(cost_time, func.__name__))
return wrapper
@timer
def add(x, y):
print('{} + {} = {}'.format(x, y, x+y))
print(add.__name__)
add(2,3)

#output:

#2 + 3 = 5
#add
#Func:add Time spent:0.0

先更到这里吧…类装饰器之类的后续再更

参考文章

详解 Python 的装饰器 Toby Qin

https://www.cnblogs.com/cicaday/p/python-decorator.html

一篇文章搞懂装饰器所有用法 Ellison 张

https://www.cnblogs.com/ellisonzhang/p/11196390.html

对 python 函数后面有多个括号的理解?Python 探索牛

https://www.cnblogs.com/djdjdj123/p/12063737.html

装饰器 廖雪峰

https://www.liaoxuefeng.com/wiki/1016959663602400/1017451662295584

Author

BakaFT

Posted on

2020-06-05

Updated on

2023-12-28

Licensed under

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×