不带参数的装饰器
我们快速回顾一下不带参数的装饰器是如何工作的,它的核心作用是在不修改原函数代码的情况下,为函数增加额外的功能。

(图片来源网络,侵删)
一个简单的装饰器 my_decorator:
def my_decorator(func):
"""一个简单的装饰器"""
def wrapper():
print("函数执行前的操作")
func() # 调用原始函数
print("函数执行后的操作")
return wrapper
@my_decorator
def say_hello():
print("Hello, World!")
# 调用 say_hello 实际上是调用 wrapper
say_hello()
输出:
函数执行前的操作
Hello, World!
函数执行后的操作
工作原理:
- 当 Python 解释器看到
@my_decorator时,它相当于执行了say_hello = my_decorator(say_hello)。 my_decorator函数接收say_hello函数作为参数,并返回一个新的函数wrapper。- 之后,
say_hello这个名字就指向了wrapper函数。
带参数的装饰器:为什么需要?
不带参数的装饰器很好用,但它的功能是固定的,上面的装饰器只能打印固定的两句话,如果我们想让它更灵活,比如可以自定义打印的消息,该怎么办?

(图片来源网络,侵删)
这时,带参数的装饰器就派上用场了,它允许我们在使用装饰器时传递参数,从而让装饰器的行为可以动态配置。
如何实现带参数的装饰器?
实现带参数的装饰器,关键在于增加一个函数层级。
一个带参数的装饰器,本质上是一个返回装饰器函数的函数,听起来有点绕,我们把它拆解开来:
- 最外层函数:接收装饰器的参数。
- 中间层函数:接收原始函数,并返回一个新的包装函数。
- 最内层函数:包装函数,包含原始函数的调用和额外的逻辑。
我们用一个例子来改造上面的 say_hello 函数,让装饰器可以接收自定义的 before_msg 和 after_msg。

(图片来源网络,侵删)
# 1. 最外层函数,接收装饰器的参数
def decorator_with_args(before_msg, after_msg):
"""这个函数返回一个真正的装饰器"""
print(f"装饰器工厂被调用,参数: before_msg='{before_msg}', after_msg='{after_msg}'")
# 2. 中间层函数,接收原始函数 func
def actual_decorator(func):
"""这是一个标准的装饰器,它包装 func"""
# 3. 最内层函数,包装函数,包含核心逻辑
def wrapper(*args, **kwargs):
print(before_msg)
result = func(*args, **kwargs) # 调用原始函数,并传递参数
print(after_msg)
return result # 返回原始函数的返回值
return wrapper
return actual_decorator
# 使用带参数的装饰器
@decorator_with_args("准备打招呼...", "打招呼完成!")
def say_hello(name):
print(f"Hello, {name}!")
return f"Successfully greeted {name}."
# 调用被装饰的函数
print("\n--- 调用 say_hello ---")
result = say_hello("Alice")
print(f"函数返回值: {result}")
输出:
装饰器工厂被调用,参数: before_msg='准备打招呼...', after_msg='打招呼完成!'
--- 调用 say_hello ---
准备打招呼...
Hello, Alice!
打招呼完成!
函数返回值: Successfully greeted Alice.
代码解析:
-
@decorator_with_args("准备打招呼...", "打招呼完成!")的执行过程:- Python 首先调用
decorator_with_args("准备打招呼...", "打招呼完成!")。 - 这个函数会打印第一行日志,然后返回
actual_decorator这个函数。 - 语句实际上等价于
say_hello = actual_decorator(say_hello)。
- Python 首先调用
-
actual_decorator(func)的执行过程:actual_decorator接收say_hello作为参数func。- 它内部定义了
wrapper函数,并返回这个wrapper。 say_hello这个名字最终指向了wrapper函数。
-
*`wrapper(args, kwargs)` 的执行过程:
- 当我们调用
say_hello("Alice")时,实际上是在调用wrapper("Alice")。 *args, **kwargs使得wrapper可以接收任意数量和类型的参数,并将其传递给原始的func。- 它执行
before_msg,然后调用func("Alice")(即say_hello("Alice")),再执行after_msg。 - 它返回
func的执行结果,使得调用链的返回值得以保留。
- 当我们调用
更通用的带参数装饰器:functools.wraps
当你使用装饰器时,原函数的元信息(如函数名 __name__、文档字符串 __doc__ 等)会丢失,因为它们被 wrapper 函数的信息覆盖了。
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper
@my_decorator
def greet():
"""这是一个文档字符串,说明函数的功能。"""
print("Hello!")
print(greet.__name__) # 输出: wrapper
print(greet.__doc__) # 输出: None
为了解决这个问题,Python 标准库 functools 提供了一个 wraps 装饰器,它专门用来修复被包装函数的元信息。
最佳实践: 在任何自定义装饰器的 wrapper 函数上都使用 @functools.wraps(func)。
import functools
def my_decorator(func):
@functools.wraps(func) # 使用 @functools.wraps
def wrapper(*args, **kwargs):
"""wrapper 函数的文档字符串"""
print("Something is happening before the function is called.")
result = func(*args, **kwargs)
print("Something is happening after the function is called.")
return result
return wrapper
@my_decorator
def greet():
"""这是一个文档字符串,说明函数的功能。"""
print("Hello!")
print(greet.__name__) # 输出: greet
print(greet.__doc__) # 输出: 这是一个文档字符串,说明函数的功能。
我们把 @functools.wraps 应用到我们带参数的装饰器中,使其成为完整、规范的版本。
import functools
def log_execution(log_level):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"[{log_level}] 函数 {func.__name__} 即将执行...")
result = func(*args, **kwargs)
print(f"[{log_level}] 函数 {func.__name__} 执行完毕。")
return result
return wrapper
return decorator
@log_execution("INFO")
def add(a, b):
"""将两个数字相加"""
return a + b
print(f"计算结果: {add(3, 5)}")
print(f"函数文档: {add.__doc__}")
输出:
[INFO] 函数 add 即将执行...
[INFO] 函数 add 执行完毕。
计算结果: 8
函数文档: 将两个数字相加
| 类型 | 结构 | 工作原理 | 示例 |
|---|---|---|---|
| 无参数装饰器 | def decorator(func): ... |
func = decorator(func) |
@timer |
| 带参数装饰器 | def decorator_with_args(args): def decorator(func): ... |
func = decorator_with_args(args)(func) |
@log(level="DEBUG") |
核心要点:
- 带参数的装饰器是一个三层的嵌套结构。
- 最外层函数接收装饰器的参数,并返回中间层的装饰器函数。
- 中间层函数接收原始函数,并返回最内层的包装函数。
@functools.wraps是一个好习惯,它能保留原函数的元信息,便于调试和文档化。- 在包装函数中使用
*args, **kwargs可以让你的装饰器适用于任何参数的函数。
掌握了带参数的装饰器,你就可以编写出高度可复用、可配置的强大功能模块,
- 性能分析器:可以传入采样频率。
- 缓存装饰器:可以设置缓存大小和过期时间。
- 权限校验:可以传入允许的角色列表。
- 日志装饰器:可以配置日志级别和输出格式。
