引子

在 Python 中有一个非常重要且有特色的语法 修饰器 ,但是修饰器的语法也非常难懂,只有更好的理解修饰器原理才能更好掌握该语法。在网上查了不少资料,但是发现很多文章仅仅介绍了修饰器的基本使用而没有介绍其 原理 ,本文将分从 基本语法实现原理 两个方面谈谈我对修饰器的理解。

基本语法

无参修饰器

1
2
3
4
5
6
7
8
9
10
11
12
# 无参修饰器的基本格式
def decorator(func):
def wrapper(*args, **kwargs):
# front operators
__ = func(*args, **kwargs)
# later operators
return __
return wrapper

@decorator
def func(*args, **kwargs):
pass # do something

下面举个时间测试修饰器的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import time

def time_it(func):
def wrapper(*args, **kwargs):
_start_time = time.time()
__ = func(*args, **kwargs)
_end_time = time.time()
print("time_it: {}".format(end_time - start_time))
return __
return wrapper

@time_it
def test(sec):
time.sleep(sec)

if __name__ == "__main__":
test()

由此也可见,Python 修饰器最大的优势就是可以方便的将代码放入一个嵌套的逻辑中,而如果我们每次需要编写这个嵌套逻辑往往很繁琐的。

带参修饰器

带参修饰器需要在无参修饰器外面再套一层壳,是不是感觉很奇怪?感觉这套语法怎么这么多嵌套?别急,我们后面会解释的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 有参修饰器的基本格式
def decorator(*args, **kwargs):
def inner(func):
def wrapper(*args_, **kwargs_):
# front operators with args and kwargs
__ = func(*args_, **kwargs_)
# later operators with args and kwargs
return __
return wrapper
return inner

@decorator(*args, **kwargs)
def func(*args, **kwargs):
pass # do something

下面举个互斥锁的例子,这样 原子操作 抽象为函数后只需简单添加修饰器即可。

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
# 卖票程序演示持锁的修饰器实现
import threading

ticket_id = 1
mutex = threading.Lock()

def WithMutex(mutex: threading.Lock):
def decorator(func):
def wrapper(*args, **kwargs):
mutex.acquire()
__ = func(*args, **kwargs)
mutex.release()
return __
return wrapper
return decorator

@WithMutex(mutex)
def atom(i: int) -> bool:
global ticket_id
if ticket_id > 100:
return False
print("Thread {} give out ticket {}".format(i, ticket_id))
ticket_id += 1
return True

def run(i: int):
while atom(i):
pass

if __name__ == "__main__":
threads = []
for i in range(5):
threads.append(threading.Thread(target=run, args=(i, )))
for i in range(5):
threads[i].start()
for i in range(5):
threads[i].join()

实现原理

感觉很抽象,非常的难记。但是解释一下原理你就懂了。

其实 无参修饰器带参修饰器 在原理上是一致的,那么解释器是如何解释这两段代码的呢?可以看到不管是 无参修饰器 还是 带参修饰器 它们都有共同的特点,返回一系列的函数。其实解释器是这样解释的:

1
2
3
4
5
6
# 原代码, 修饰器实现省略
@decorator
def func(*args, **kwargs):
pass # do something

func(*args, **kwargs)
1
2
3
4
5
# 解释器等效代码
decorator(func)(*args, **kwargs)
# 也可以理解为
# wrapper = decorator(func)
# wrapper(*args, **kwargs)

我们来仔细理解一下这份代码。decorator(func) 返回了一个内嵌函数 wrapper(函数也是对象,请区别函数与函数返回值),而内嵌函数实际获取了参数 *args, **kwargs。这里还值得一提的是,内嵌函数的实现允许其保留外部函数的变量,通常外部函数生命周期结束后其局部变量都将释放,而特别的是如果存在内嵌函数被保留,外部函数的局部变量不会释放。在这个示例中,func 以参数的形式传入外部函数 decorator,而在内嵌函数 wrapper 中依然可用。

这样我们也可以很好解释 带参修饰器 ,就是多嵌套了一层函数:

1
2
3
4
5
6
# 原代码, 修饰器实现省略
@decorator(*args, **kwargs)
def func(*args_, **kwargs_):
pass # do something

func(*args_, **kwargs_)
1
2
# 解释器等效代码
decorator(*args, **kwargs)(func)(*args_, **kwargs_)
我个人对于修饰器的一些吐槽

尽管 Python 修饰器的语法在频繁调用 “修饰” 功能时很方便,但不可否认实现一个修饰器的代码非常麻烦而且难读。

虽然实现修饰器的代码逻辑非常符合其实现原理,但 Python 完全可以使用一套更方便的语法实现修饰器。