Python 进阶之 Decorators(装饰器)浅析

Decorators(装饰器)可以在不更改函数或对象的行为的前提下,动态地向其添加额外的效果

假设当前的项目中有多个函数需要添加日志功能,即函数执行时向终端或者日志文件中输出特定的内容。

有一种办法就是在每一个函数中添加上若干行记录日志的代码,但这种方式耗费时间的同时,也容易出现意想不到的错误,毕竟会对原本的代码做出相当大的改动。
而另一办法就是在每一个函数或类前面添加装饰器,通过装饰器向被装饰函数添加额外的行为(记录日志),这样在提升效率的同时,也不会导致现有的代码中引入了新的 bug 。

比较常见的一种装饰器,比如下面的一段最简单的 flask 应用代码:

1
2
3
4
5
6
7
8
9
from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
return "Hello World"

if __name__ == '__main__':
app.run()

上面的代码通过 route 装饰器向 hello 函数添加了某些效果,在不变更原 hello 函数内部代码的前提下,将其变成了某个新函数作为 Web 应用中的 API 接受调用 。

一、初级示例

函数作为参数

下面是一段简单的函数代码,可以用来将某个字符串转换为大写:

1
2
3
4
5
6
7
8
9
def to_uppercase(text):
if not isinstance(text, str):
raise TypeError("Not a string")
return text.upper()

text = "Hello World"
upper_text = to_uppercase(text)
print(upper_text)
# => HELLO WORLD

这里对 to_uppercase 函数做一点微小的改动:

1
2
3
4
5
6
7
8
9
10
11
12
13
def to_uppercase(func):
text = func()

if not isinstance(text, str):
raise TypeError("Not a string")
return text.upper()

def hello():
return "Hello World"

hello = to_uppercase(hello)
print(hello)
# => HELLO WORLD

与之前版本的 to_uppercase 不同,此版本的 to_uppercase 函数并不直接使用字符串作为输入,而是接收某个函数作为参数,将该函数执行后返回的字符串转换为大写。

to_uppercase 函数并没有改变 hello 函数原本的行为(输出字符串),而是在其基础上添加了额外的效果(将输出字符串转换为大写),因而起到了装饰器的作用。

上面的代码也可以写成如下的形式(两者效果相同):

1
2
3
4
5
6
7
8
9
10
11
12
13
def to_uppercase(func):
text = func()

if not isinstance(text, str):
raise TypeError("Not a string")
return text.upper()

@to_uppercase
def hello():
return "Hello World"

print(hello)
# => HELLO WORLD

@decorator 是 Python 中的语法糖,

1
2
3
@decorator
def func():
...

等同于

1
2
3
def func():
...
func = decorator(func)

函数作为返回值

以下代码为 to_uppercase 装饰器的最终形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def to_uppercase(func):
def wrapper():
text = func()
if not isinstance(text, str):
raise TypeError("Not a string type")
return text.upper()
return wrapper

@to_uppercase
def hello():
return "Hello World"
'''
等同于
def hello():
return "Hello World"
hello = to_uppercase(hello)
'''

print(hello())
# => HELLO WORLD

之前的 to_uppercase 函数接收另一个函数作为参数,获取其返回值并作出修改,最后返回修改后的结果。
而此处的 to_uppercase 函数在代码中内嵌了一个 wrapper 函数并将其作为返回值,wrapper 函数中包含了对被装饰的函数做出的改动。
to_uppercase 装饰器接收被装饰的函数作为参数,通过内嵌函数对其进行改动,最终返回一个新的函数替代被装饰的原函数。

回到代码中,hello 函数用于返回 Hello World 字符串,而装饰器 to_uppercase 接收 hello 作为参数,通过 wrapper 对其添加新的行为(将返回的字符串转为大写)并替换掉原来的 hello 函数。
因此在不改变原 hello 函数内部代码的情况下,通过装饰器生成了新的 hello 函数,最终改变了原函数的行为。

二、使用多个装饰器

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def add_prefix(func):
def wrapper():
text = func()
result = " ".join([text, "Larry Page!"])
return result
return wrapper

def to_uppercase(func):
def wrapper():
text = func()
if not isinstance(text, str):
raise TypeError("Not a string")
return text.upper()
return wrapper()

@to_uppercase
@add_prefix
def say():
return "welcome"

print(say)
# => WELCOME LARRY PAGE!

三、带参数的装饰器

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def to_uppercase(func):
def wrapper(*args, **kwargs):
text = func(*args, **kwargs)
if not isinstance(text, str):
raise TypeError("Not a string")
return text.upper()
return wrapper

@to_uppercase
def say(greet):
return greet

print(say("hello, how are you"))
# => HELLO, HOW ARE YOU

四、functools.wraps

运行如下装饰器代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def logging(func):
def logs(*args, **kwargs):
print(func.__name__ + " was called")
return func(*args, **kwargs)
return logs

@logging
def foo(x):
"""Calling function for logging"""
return x * x

fo = foo(10)
# => foo was called
print(foo.__name__)
# => logs
print(foo.__doc__)
# => None

从运行结果中可以看出,print(foo.__name__) 并没有输出 foo ,而是打印了装饰器的内嵌函数 logs 的名字。
即被装饰的函数 foo 由新函数替代后,其 __name____doc__ 等属性也丢失了。

为了避免这种情况,可以使用 functool.wrap ,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from functools import wraps
def logging(func):
@wraps(func)
def logs(*args, **kwargs):
print(func.__name__ + " was called")
return func(*args, **kwargs)
return logs

@logging
def foo(x):
"""does some math"""
return x * x

fo = foo(10)
# => foo was called
print(foo.__name__)
# => foo
print(foo.__doc__)
# => does some math

五、场景:基于装饰器的授权

很多 Web API 都需要用户携带认证信息才能访问,当然可以在每一段 API 的代码中加入检查授权状态的片段,更便捷的方式则是使用装饰器。如:

1
2
3
4
5
6
7
8
9
10
from functools import wraps

def requires_auth(func):
@wraps(func)
def wrapper(*args, **kwargs):
auth = request.authorization
if not auth or not check_auth(auth.username, auth.password):
authenticate()
return func(*args, **kwargs)
return wrapper

则每一个被 require_auth 装饰的函数执行前,都会先获取授权信息并验证。

参考资料

Clean Python
Python 函数装饰器