35·模块八:装饰器与高级主题高级

带参数的装饰器

decoratorsarguments

第35课:带参数的装饰器

学习目标

完成本课学习后,你将能够:

  1. 理解为什么需要使用带参数的装饰器(参数化装饰器)。
  2. 掌握使用三层嵌套函数实现带参数装饰器的核心模式。
  3. 学会在带参数的装饰器中使用 functools.wraps 来保持被装饰函数的元信息。
  4. 能够将带参数的装饰器应用到实际场景中,例如日志记录和权限验证。

核心概念

在上一课中,我们学习了装饰器的基本原理:通过一个函数包装另一个函数,在不修改原函数代码的情况下,为其添加新功能。但有时,我们希望装饰器本身能接受一些"配置",以便更灵活地改变其行为。这就是带参数的装饰器的作用。

通俗理解: 想象你买了一个"函数装修"服务(基础装饰器)。现在,你想告诉装修队:"这次请按中式风格来装修"(传递参数style='中式')。带参数的装饰器就像是这个接受你具体要求的"高级装修队"。

从代码结构上看,一个不带参数的装饰器通常是一个两层嵌套的函数:

  1. 外层函数接收被装饰的函数 func
  2. 内层函数(包装器)在内部调用 func 并添加功能。

而带参数的装饰器则是一个三层嵌套的函数:

  1. 最外层函数(装饰器工厂)接收装饰器的参数(如 style),并返回真正的装饰器。
  2. 中间层函数(真正的装饰器)接收被装饰的函数 func
  3. 最内层函数(包装器)执行实际的包装逻辑。

简单来说:装饰器工厂 -> 创建一个 装饰器 -> 该装饰器返回一个 包装函数

代码示例

示例 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,用于指定被装饰的函数需要重复执行的次数。 要求

  1. 函数只返回最后一次执行的结果。
  2. 打印出每次执行的轮次信息。
# 你的代码开始
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。

常见错误

  1. 忘记返回函数:在最外层和中间层函数末尾,一定要分别返回 decoratorwrapper。如果忘记返回,装饰器将失效。

    # 错误示例
    def bad_decorator(param):
        def decorator(func):
            def wrapper(*args, **kwargs):
                # ... 逻辑
                pass
            # 缺少 return wrapper
        # 缺少 return decorator
    
  2. 参数传递层级错误:装饰器的参数在最外层接收,被装饰的函数在中间层接收。如果在最内层试图接收装饰器参数,会导致错误。

  3. 丢失原函数信息:如果没有使用 @functools.wraps(func)wrapper.__name__wrapper.__doc__ 会变成装饰器内部函数的信息,这可能会给调试和文档生成带来困扰。

  4. 调用时忘记加括号:当使用带参数的装饰器时,@decorator 必须写成 @decorator()@decorator(param=value) 的形式。写成 @decorator 会抛出 TypeError,因为解释器尝试将被装饰的函数作为第一个参数传递给 decorator

小结

本课我们学习了装饰器的高级形式——带参数的装饰器。关键要点如下:

  1. 结构模式:它本质上是一个"装饰器工厂"函数,通过三层嵌套实现。最外层接收参数,中间层接收函数,最内层是包装逻辑。
  2. 执行流程装饰器工厂(参数) 返回 装饰器 -> 装饰器(原函数) 返回 包装函数 -> 调用 包装函数() 时执行增强逻辑并调用原函数。
  3. 重要技巧:始终使用 @functools.wraps(func) 来装饰最内层的 wrapper 函数,以保持原函数的名称、文档字符串等元信息。
  4. 应用场景:当你需要让装饰器的行为可配置时,例如设定日志级别、指定缓存策略、控制重试次数等,带参数的装饰器是非常强大的工具。

掌握了带参数的装饰器,你就拥有了编写高度可复用和可配置代码组件的能力,这是迈向Python高级编程的重要一步。下一课,我们将学习支撑装饰器工作的重要概念——闭包

练习编辑器

python
Loading...

继续学习

完成本课后,建议继续学习下一课「闭包(Closures)及其应用」 以巩固所学知识。