装饰器回顾:无参数
我们快速回顾一下最简单的装饰器,装饰器的本质是一个函数,它接收另一个函数作为参数(被装饰的函数),并返回一个新的函数,这个新函数通常会增强或替换原始函数的功能。

核心概念:A(B) -> C
B是你的原始函数。A是装饰器函数。C是返回的新函数,它包含了B的功能,并可能增加了额外的逻辑。
简单例子:计时装饰器
import time
# 这是一个装饰器函数
def timer(func):
# wrapper 是一个内部函数,它会替换原始函数
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs) # 执行原始函数
end_time = time.time()
print(f"函数 {func.__name__} 执行耗时: {end_time - start_time:.4f} 秒")
return result # 返回原始函数的执行结果
return wrapper
# 使用 @timer 语法来装饰 my_function
@timer
def my_function(n):
"""一个简单的耗时函数"""
total = 0
for i in range(n):
total += i
return total
# 调用 my_function,实际上是在调用 wrapper 函数
print(f"计算结果: {my_function(1000000)}")
流程分析:
- 当 Python 看到
@timer时,它会执行timer(my_function)。 my_function函数对象被传递给timer函数。timer函数内部定义了wrapper函数,然后返回wrapper这个函数对象。my_function这个名字不再指向原来的函数,而是指向了timer返回的wrapper函数。- 当你调用
my_function(...)时,实际上是在调用wrapper(...)。
带参数的装饰器
让我们进入正题:如何给装饰器本身传递参数?

需求场景:
假设我们想创建一个日志装饰器,我们可以指定日志的级别,"INFO" 或 "DEBUG"。
# 我们希望这样使用:
@log_with_level(level="INFO")
def add(a, b):
return a + b
@log_with_level(level="DEBUG")
def multiply(a, b):
return a * b
这里的 level="INFO" 就是传递给装饰器 log_with_level 的参数。
实现思路与难点: 如果你直接尝试,会发现行不通:
# 错误的尝试
def log_with_level(level):
def timer(func): # 错误!这里的 func 不存在
def wrapper(*args, **kwargs):
print(f"[{level}] 正在执行函数 {func.__name__}...")
result = func(*args, **kwargs)
print(f"[{level}] 函数 {func.__name__} 执行完毕")
return result
return wrapper
return timer # 返回的是 timer 函数
问题在于 @log_with_level(level="INFO") 这个语法,Python 会立即执行 log_with_level(level="INFO"),这个函数的返回值(在这里是 timer 函数)会应用到被装饰的函数(add)上。
正确的结构是三层嵌套函数:
- 最外层函数:接收装饰器的参数(如
level),并返回一个真正的装饰器。 - 中间层函数:接收被装饰的函数(如
add),并返回一个包装函数。 - 最内层函数:执行真正的功能增强逻辑。
正确实现:三层嵌套函数
def log_with_level(level):
"""第一层:接收装饰器的参数,并返回一个装饰器"""
print(f"装饰器工厂被调用,参数 level={level}")
def decorator(func):
"""第二层:接收被装饰的函数,并返回一个包装函数"""
def wrapper(*args, **kwargs):
"""第三层:执行核心逻辑"""
print(f"[{level}] 正在执行函数 {func.__name__}...")
result = func(*args, **kwargs)
print(f"[{level}] 函数 {func.__name__} 执行完毕")
return result
return wrapper
return decorator
# 使用
@log_with_level(level="INFO")
def add(a, b):
"""加法函数"""
return a + b
@log_with_level(level="DEBUG")
def multiply(a, b):
"""乘法函数"""
return a * b
print("-" * 20)
print("调用 add 函数:")
result_add = add(3, 5)
print(f"结果: {result_add}")
print("-" * 20)
print("调用 multiply 函数:")
result_multiply = multiply(4, 6)
print(f"结果: {result_multiply}")
执行流程分析:
- Python 解释器从上到下执行代码。
- 当它遇到
@log_with_level(level="INFO")时,会立即调用log_with_level(level="INFO")。 log_with_level函数执行完毕,返回了decorator这个函数。add函数的定义就变成了add = decorator。- 当定义
multiply时,同理,multiply = decorator(这里的decorator是在log_with_level(level="DEBUG")调用后返回的)。 - 关键点:
level的值("INFO"或"DEBUG")被“捕获”并保存在闭包中,供wrapper函数后续使用。
functools.wraps 的重要性
当你使用多层嵌套的装饰器时,原始函数的元信息(如函数名 __name__、文档字符串 __doc__)会丢失,因为它们被 wrapper 函数覆盖了。
def my_decorator(func):
def wrapper(*args, **kwargs):
"""这是 wrapper 函数的文档字符串"""
print("Something is happening before the function is called.")
func(*args, **kwargs)
print("Something is happening after the function is called.")
return wrapper
@my_decorator
def say_hello():
"""这是 say_hello 函数的文档字符串"""
print("Hello!")
print(say_hello.__name__) # 输出: wrapper
print(say_hello.__doc__) # 输出: 这是 wrapper 函数的文档字符串
为了解决这个问题,Python 标准库 functools 提供了一个 wraps 装饰器,你应该把它用在你的包装函数上,它会将被装饰函数的元信息复制到包装函数上。
使用 functools.wraps 的最佳实践
import functools
import time
def timer_with_args(arg1, arg2):
def decorator(func):
@functools.wraps(func) # 关键!
def wrapper(*args, **kwargs):
print(f"装饰器接收到的参数: arg1={arg1}, arg2={arg2}")
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"函数 {func.__name__} 执行耗时: {end_time - start_time:.4f} 秒")
return result
return wrapper
return decorator
@timer_with_args("这是参数1", 100)
def calculate_sum(n):
"""计算从 0 到 n-1 的总和"""
return sum(range(n))
print(f"函数名: {calculate_sum.__name__}")
print(f"文档字符串: {calculate_sum.__doc__}")
print("-" * 20)
result = calculate_sum(1000000)
print(f"计算结果: {result}")
输出:
函数名: calculate_sum
文档字符串: 计算从 0 到 n-1 的总和
--------------------
装饰器接收到的参数: arg1=这是参数1, arg2=100
函数 calculate_sum 执行耗时: 0.0289 秒
计算结果: 499999500000
可以看到,calculate_sum 的 __name__ 和 __doc__ 都被正确地保留了。
总结与比喻
为了更好地理解,我们可以用一个比喻:
-
无参数装饰器:像一个外套,你直接把外套(装饰器)穿在函数(衣服)外面。
@timer-> 穿上timer外套。
-
带参数的装饰器:像一个可定制的工具箱,你不能直接把工具箱变成外套,你需要先告诉工具箱你需要什么工具(装饰器参数),然后工具箱会给你一个具体的、可用的工具(真正的装饰器),你再用这个工具来包装你的函数。
@log_with_level(level="INFO")-> 打开工具箱,告诉它“我需要一个 INFO 级别的日志工具”,工具箱(log_with_level)递给你一个配置好的日志工具(decorator),你用这个工具来包装你的函数。
核心要点:
- 语法糖:
@decorator等价于my_func = decorator(my_func)。 - 带参数的装饰器是三层的:
- 最外层:接收参数,返回一个装饰器。
- 中间层:接收函数,返回一个包装器。
- 最内层:执行逻辑。
- 闭包:中间层和最内层函数形成闭包,使得最内层函数可以访问到最外层传入的参数。
- 最佳实践:始终在包装器函数上使用
@functools.wraps(func)来保留原始函数的元信息。
掌握了带参数的装饰器,你就掌握了 Python 中一个非常强大且灵活的工具,可以用来编写高度可配置和可复用的代码。
