第35课:带参数的装饰器
学习目标
完成本课学习后,你将能够:
- 理解为什么需要使用带参数的装饰器(参数化装饰器)。
- 掌握使用三层嵌套函数实现带参数装饰器的核心模式。
- 学会在带参数的装饰器中使用
functools.wraps来保持被装饰函数的元信息。 - 能够将带参数的装饰器应用到实际场景中,例如日志记录和权限验证。
核心概念
在上一课中,我们学习了装饰器的基本原理:通过一个函数包装另一个函数,在不修改原函数代码的情况下,为其添加新功能。但有时,我们希望装饰器本身能接受一些"配置",以便更灵活地改变其行为。这就是带参数的装饰器的作用。
通俗理解:
想象你买了一个"函数装修"服务(基础装饰器)。现在,你想告诉装修队:"这次请按中式风格来装修"(传递参数style='中式')。带参数的装饰器就像是这个接受你具体要求的"高级装修队"。
从代码结构上看,一个不带参数的装饰器通常是一个两层嵌套的函数:
- 外层函数接收被装饰的函数
func。 - 内层函数(包装器)在内部调用
func并添加功能。
而带参数的装饰器则是一个三层嵌套的函数:
- 最外层函数(装饰器工厂)接收装饰器的参数(如
style),并返回真正的装饰器。 - 中间层函数(真正的装饰器)接收被装饰的函数
func。 - 最内层函数(包装器)执行实际的包装逻辑。
简单来说:装饰器工厂 -> 创建一个 装饰器 -> 该装饰器返回一个 包装函数。
代码示例
示例 1:一个简单的带参数装饰器
这个装饰器会根据传入的日志级别,在函数执行前后打印不同级别的日志。
import functools
def log_with_level(level): # 最外层:装饰器工厂,接收装饰器参数
def decorator(func): # 中间层:真正的装饰器,接收被装饰的函数
@functools.wraps(func) # 保留原函数的元信息(如__name__, __doc__)
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("WARNING")
def calculate_area(length, width):
"""计算矩形面积"""
return length * width
# 测试函数
area = calculate_area(5, 3)
print(f"面积是:{area}")
print(f"函数名:{calculate_area.__name__}") # 使用@functools.wraps后,名字仍是原函数的
print(f"文档字符串:{calculate_area.__doc__}")
# 输出:
# [WARNING] 函数 calculate_area 即将执行。
# [WARNING] 函数 calculate_area 执行完毕。
# 面积是:15
# 函数名:calculate_area
# 文档字符串:计算矩形面积
示例 2:一个更灵活的装饰器——计时器
这个装饰器允许你选择是否在控制台打印执行时间。
import time
import functools
def timer(print_time=True): # 参数有一个默认值
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
elapsed = end_time - start_time
if print_time:
print(f"函数 {func.__name__} 耗时: {elapsed:.4f} 秒")
return result
return wrapper
return decorator
@timer(print_time=True) # 显式指定参数
def process_data(data):
"""模拟数据处理"""
time.sleep(0.1)
return [d * 2 for d in data]
@timer() # 使用默认参数 print_time=True
def another_process():
time.sleep(0.05)
process_data([1, 2, 3])
another_process()
# 输出:
# 函数 process_data 耗时: 0.1005 秒
# 函数 another_process 耗时: 0.0503 秒
实践练习
练习 1:基础应用
创建一个名为 repeat 的装饰器,它接受一个参数 times,用于指定被装饰的函数需要重复执行的次数。
要求:
- 函数只返回最后一次执行的结果。
- 打印出每次执行的轮次信息。
# 你的代码开始
def repeat(times):
# 补充完整
pass
# 你的代码结束
@repeat(times=3)
def greet(name):
print(f"你好,{name}!")
return f"问候完成: {name}"
result = greet("Alice")
print(f"最终返回值: {result}")
预期输出:
[第1次执行] 你好,Alice!
[第2次执行] 你好,Alice!
[第3次执行] 你好,Alice!
最终返回值: 问候完成: Alice
练习 2:进阶挑战
实现一个 validate_args 装饰器,它接受一个参数 validation_func,这是一个用于验证函数输入参数的函数。如果验证失败(validation_func返回False),则直接返回 None 并打印错误信息,否则正常执行原函数。
# 你的代码开始
def validate_args(validation_func):
# 补充完整
pass
# 你的代码结束
def is_positive_numbers(*args):
"""验证所有参数都是正数"""
return all(isinstance(a, (int, float)) and a > 0 for a in args)
@validate_args(is_positive_numbers)
def add_positive(a, b):
return a + b
print(add_positive(2, 3)) # 应输出 5
print(add_positive(2, -3)) # 应输出 None 并打印错误
预期输出:
5
参数验证失败: 函数 add_positive 收到了无效的参数。
None
练习 3:综合应用(选做)
设计一个 limit_calls 装饰器,它接受一个参数 max_calls,限制函数最多被调用 max_calls 次。超过次数后,调用函数将打印警告信息并返回 None。
# 提示:你需要使用闭包或类来维护一个调用计数器。
@limit_calls(max_calls=2)
def say_hello():
print("Hello!")
say_hello() # 正常执行
say_hello() # 正常执行
say_hello() # 打印警告: "函数 say_hello 已达到最大调用次数(2次)",并返回None。
常见错误
-
忘记返回函数:在最外层和中间层函数末尾,一定要分别返回
decorator和wrapper。如果忘记返回,装饰器将失效。# 错误示例 def bad_decorator(param): def decorator(func): def wrapper(*args, **kwargs): # ... 逻辑 pass # 缺少 return wrapper # 缺少 return decorator -
参数传递层级错误:装饰器的参数在最外层接收,被装饰的函数在中间层接收。如果在最内层试图接收装饰器参数,会导致错误。
-
丢失原函数信息:如果没有使用
@functools.wraps(func),wrapper.__name__和wrapper.__doc__会变成装饰器内部函数的信息,这可能会给调试和文档生成带来困扰。 -
调用时忘记加括号:当使用带参数的装饰器时,
@decorator必须写成@decorator()或@decorator(param=value)的形式。写成@decorator会抛出TypeError,因为解释器尝试将被装饰的函数作为第一个参数传递给decorator。
小结
本课我们学习了装饰器的高级形式——带参数的装饰器。关键要点如下:
- 结构模式:它本质上是一个"装饰器工厂"函数,通过三层嵌套实现。最外层接收参数,中间层接收函数,最内层是包装逻辑。
- 执行流程:
装饰器工厂(参数)返回装饰器->装饰器(原函数)返回包装函数-> 调用包装函数()时执行增强逻辑并调用原函数。 - 重要技巧:始终使用
@functools.wraps(func)来装饰最内层的wrapper函数,以保持原函数的名称、文档字符串等元信息。 - 应用场景:当你需要让装饰器的行为可配置时,例如设定日志级别、指定缓存策略、控制重试次数等,带参数的装饰器是非常强大的工具。
掌握了带参数的装饰器,你就拥有了编写高度可复用和可配置代码组件的能力,这是迈向Python高级编程的重要一步。下一课,我们将学习支撑装饰器工作的重要概念——闭包。