Python 装饰器从入门到精通:带参数装饰器与类装饰器实战

5次阅读
没有评论

共计 7858 个字符,预计需要花费 20 分钟才能阅读完成。

Python 装饰器是 Python 语言中一种强大且优雅的语法糖,它允许程序员在不修改原函数代码的情况下,增加函数的功能。从日志记录、性能监控到权限验证,装饰器无处不在,是编写高可维护性、高复用性代码的关键工具。本文将带你从装饰器的基本概念出发,深入探索带参数装饰器和类装饰器的高级用法,并通过实战案例,助你彻底掌握 Python 装饰器的精髓。

装饰器基础回顾

在深入带参数装饰器和类装饰器之前,我们先快速回顾一下装饰器的基本原理。

装饰器本质上是一个接受函数作为参数,并返回一个新函数的函数。它的核心思想是“封装”和“增强”。Python 的 @ 语法糖让这个过程变得非常简洁。

考虑一个最简单的装饰器,用于打印函数执行日志:

def log_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"--- 函数 {func.__name__} 开始执行 ---")
        result = func(*args, **kwargs)
        print(f"--- 函数 {func.__name__} 执行完毕 ---")
        return result
    return wrapper

@log_decorator
def greet(name):
    print(f"Hello, {name}!")
    return f"Greeted {name}"

@log_decorator
def add(a, b):
    print(f"Calculating {a} + {b}...")
    return a + b

greet("Alice")
print(add(10, 20))

执行流程解析:

  1. 当 Python 解释器遇到 @log_decorator 语法时,它会执行 log_decorator(greet)
  2. log_decorator 函数接收 greet 函数作为参数 func
  3. log_decorator 定义了一个内部函数 wrapper,该 wrapper 才是真正替代 greet 被调用的函数。
  4. log_decorator 返回这个 wrapper 函数。
  5. 此时,greet 这个名字实际上指向了 wrapper 函数。
  6. 当我们调用 greet("Alice") 时,实际执行的是 wrapper("Alice")
  7. wrapper 函数内部先打印日志,然后调用原始的 greet 函数,再打印结束日志,并返回结果。

为了避免装饰器覆盖原函数的元信息(如 __name__, __doc__ 等),我们通常会使用 functools.wraps

import functools

def log_decorator_with_wraps(func):
    @functools.wraps(func) # 使用 functools.wraps 拷贝原函数的元信息
    def wrapper(*args, **kwargs):
        print(f"--- 函数 {func.__name__} 开始执行 ---")
        result = func(*args, **kwargs)
        print(f"--- 函数 {func.__name__} 执行完毕 ---")
        return result
    return wrapper

@log_decorator_with_wraps
def say_hello(name):
    """一个简单的打招呼函数"""
    return f"Hello, {name}!"

print(say_hello.__name__)  # 输出: say_hello (而不是 wrapper)
print(say_hello.__doc__)   # 输出: 一个简单的打招呼函数

掌握了基础,我们就可以向更复杂的装饰器迈进了。

进阶:带参数的装饰器

有时,我们希望装饰器本身也能接受一些配置参数,而不仅仅是固定行为。例如,一个权限验证装饰器可能需要知道它应该验证哪个角色。这时,我们就需要带参数的装饰器。

带参数的装饰器比普通装饰器多了一个层级。它是一个 返回装饰器的函数

结构解析:

  1. 最外层函数 (参数接收层): 接收装饰器所需的参数。
  2. 中间层函数 (装饰器工厂): 接收被装饰的函数作为参数。
  3. 最内层函数 (实际包装器): 真正替代原函数被调用的部分,其中包含增强逻辑和对原函数的调用。

我们来创建一个权限验证装饰器作为例子:

import functools

def auth_required(roles):
    """
    一个带参数的权限验证装饰器。roles: 允许访问的角色列表
    """
    def decorator(func): # 这是一个标准的装饰器(接收一个函数)@functools.wraps(func)
        def wrapper(user_role, *args, **kwargs): # wrapper 接收被装饰函数调用的参数
            if user_role in roles:
                print(f"用户角色'{user_role}'已通过权限验证,允许执行 {func.__name__}")
                return func(user_role, *args, **kwargs) # 注意这里需要将 user_role 传递下去
            else:
                print(f"用户角色'{user_role}'无权访问 {func.__name__},所需角色:{roles}")
                raise PermissionError("Permission denied")
        return wrapper
    return decorator

# 使用带参数的装饰器
@auth_required(roles=['admin', 'editor'])
def create_article(user_role, title, content):
    print(f"用户 {user_role} 正在创建文章:'{title}'")
    return f"文章'{title}'已创建"

@auth_required(roles=['admin'])
def delete_user(user_role, user_id):
    print(f"用户 {user_role} 正在删除用户: {user_id}")
    return f"用户 {user_id} 已删除"

# 测试
try:
    print(create_article('admin', '我的新文章', '这是内容'))
    print(create_article('editor', '编辑文章', '更新内容'))
    # 以下会触发 PermissionError
    # print(create_article('viewer', '只读文章', '无法创建'))
except PermissionError as e:
    print(f"错误: {e}")

try:
    print(delete_user('admin', 101))
    # print(delete_user('editor', 102)) # 触发 PermissionError
except PermissionError as e:
    print(f"错误: {e}")

执行流程解析:

  1. 当解释器遇到 @auth_required(roles=['admin', 'editor']) 时:
    • 首先,auth_required(['admin', 'editor']) 被调用。它接收 roles 参数。
    • auth_required 函数返回内部的 decorator 函数。
  2. 此时,@ 语法糖就相当于 @decorator 应用到了 create_article 上。
  3. decorator(create_article) 被调用。
    • 它接收 create_article 作为 func 参数。
    • 它定义并返回内部的 wrapper 函数。
  4. 最终,create_article 这个名字现在指向了 wrapper 函数。
  5. 当我们调用 create_article('admin', '我的新文章', '这是内容') 时:
    • 实际执行的是 wrapper('admin', '我的新文章', '这是内容')
    • wrapper 函数检查 user_role 是否在 roles 中。
    • 如果验证通过,就调用原始的 create_article 函数;否则抛出 PermissionError

这种三层嵌套的结构是实现带参数函数装饰器的标准模式。

类装饰器:用类实现更强大的装饰器

当装饰器需要维护状态、或者需要更复杂的逻辑、甚至想利用面向对象特性(如继承、多态)时,使用类来实现装饰器会是一个更清晰、更强大的选择。

类装饰器主要有两种实现方式。

方法一:基于 __call__ 方法的类装饰器 (无参数)

当一个类的实例被当作函数调用时,Python 会自动执行它的 __call__ 方法。我们可以利用这个特性来实现类装饰器。

实现思路:

  1. 类在 __init__ 方法中接收被装饰的函数。
  2. 类在 __call__ 方法中实现装饰器的增强逻辑,并在其中调用被装饰的函数。

我们来实现一个简单的函数执行时间统计装饰器:

import time
import functools

class TimerDecorator:
    def __init__(self, func):
        """在类实例化时(即装饰器被应用时),接收被装饰的函数。"""
        functools.wraps(func)(self) # 拷贝元信息,使类实例表现得像被装饰的函数
        self.func = func

    def __call__(self, *args, **kwargs):
        """当被装饰的函数被调用时,实际执行的是这个 __call__ 方法。"""
        start_time = time.time()
        result = self.func(*args, **kwargs)
        end_time = time.time()
        print(f"函数'{self.func.__name__}'执行耗时: {end_time - start_time:.4f} 秒")
        return result

@TimerDecorator
def long_running_task(n):
    """一个模拟耗时任务的函数"""
    print(f"开始执行耗时任务,计算到 {n}")
    sum_val = 0
    for i in range(n):
        sum_val += i
    time.sleep(0.1) # 模拟 IO 等待
    print(f"耗时任务完成,结果 {sum_val}")
    return sum_val

@TimerDecorator
def short_task():
    print("执行一个短任务...")
    time.sleep(0.05)
    return "短任务完成"

long_running_task(1000000)
short_task()

print(long_running_task.__name__)
print(long_running_task.__doc__)

执行流程解析:

  1. 当解释器遇到 @TimerDecorator 时,它会执行 TimerDecorator(long_running_task)
  2. TimerDecorator 类的 __init__ 方法被调用,接收 long_running_task 作为 func。此时,self.func 指向了原始函数。functools.wraps(func)(self) 这行代码是为了让 long_running_task 的元数据(如 __name__, __doc__)转移到 TimerDecorator 实例本身。
  3. long_running_task 这个名字现在指向了 TimerDecorator 的一个实例。
  4. 当我们调用 long_running_task(1000000) 时,实际是调用了这个 TimerDecorator 实例,从而触发了其实例的 __call__ 方法。
  5. __call__ 方法执行计时逻辑,然后通过 self.func(*args, **kwargs) 调用原始函数,并返回结果。

方法二:带参数的类装饰器

将类装饰器与参数结合,就达到了类装饰器最强大的形态。这与带参数的函数装饰器类似,也需要一个额外的层级。

实现思路:

  1. 类在 __init__ 方法中接收装饰器所需的参数。
  2. 类实例本身 不是直接替换被装饰的函数 ,而是当它被调用(作为装饰器时)时, 返回一个真正的包装函数
  3. 这个包装函数就是实际替代原函数被调用的部分。

让我们重写一个权限验证装饰器,使用类实现并带参数:

import functools

class RoleAuthDecorator:
    def __init__(self, allowed_roles):
        """类实例化时,接收装饰器参数(允许的角色列表)。"""
        self.allowed_roles = allowed_roles

    def __call__(self, func):
        """
        当类实例被用作装饰器时,它接收被装饰的函数,并返回一个包装器函数。"""
        @functools.wraps(func)
        def wrapper(user_role, *args, **kwargs):
            if user_role in self.allowed_roles:
                print(f"用户'{user_role}'(允许角色: {self.allowed_roles}) 通过验证,执行 {func.__name__}")
                return func(user_role, *args, **kwargs)
            else:
                print(f"用户'{user_role}'无权访问 {func.__name__},所需角色:{self.allowed_roles}")
                raise PermissionError("Permission denied")
        return wrapper

# 使用带参数的类装饰器
@RoleAuthDecorator(allowed_roles=['admin'])
def delete_critical_data(user_role, item_id):
    print(f"管理员 {user_role} 正在删除关键数据: {item_id}")
    return f"关键数据 {item_id} 已删除"

@RoleAuthDecorator(allowed_roles=['admin', 'manager'])
def approve_request(user_role, request_id):
    print(f"用户 {user_role} 正在审批请求: {request_id}")
    return f"请求 {request_id} 已审批"

# 测试
try:
    print(delete_critical_data('admin', 'server-log-001'))
    # print(delete_critical_data('manager', 'server-log-002')) # 触发 PermissionError
except PermissionError as e:
    print(f"错误: {e}")

try:
    print(approve_request('admin', 'req-123'))
    print(approve_request('manager', 'req-456'))
    # print(approve_request('guest', 'req-789')) # 触发 PermissionError
except PermissionError as e:
    print(f"错误: {e}")

执行流程解析:

  1. 当解释器遇到 @RoleAuthDecorator(allowed_roles=['admin']) 时:
    • RoleAuthDecorator(['admin']) 被调用,创建一个 RoleAuthDecorator 实例。
    • 这个实例的 __init__ 方法被调用,self.allowed_roles 被设置为 ['admin']
    • 注意:此时 delete_critical_data 函数还没有被传递给装饰器。 delete_critical_data__name____doc__ 等元数据也不会被这个实例直接覆盖。
  2. 接下来,这个 RoleAuthDecorator 实例被 Python 解释器当作一个函数来调用,并将 delete_critical_data 函数作为参数传递给它,即 instance(delete_critical_data)
  3. 于是,实例的 __call__ 方法被调用,接收 delete_critical_data 作为 func
  4. __call__ 方法定义并返回 wrapper 函数。这个 wrapper 函数通过闭包捕获了 self.allowed_rolesfunc
  5. 最终,delete_critical_data 这个名字现在指向了 wrapper 函数。
  6. 当我们调用 delete_critical_data('admin', 'server-log-001') 时,实际执行的是 wrapper('admin', 'server-log-001')wrapper 内部会使用 self.allowed_roles(来自类实例的状态)进行权限检查。

这种模式的优点在于,类实例可以保存状态(如 allowed_roles),并且可以在 __init__ 中进行一些初始化或配置,让装饰器更加灵活和强大。

何时使用哪种装饰器?

选择合适的装饰器类型对于代码的清晰度和可维护性至关重要:

  • 简单函数装饰器 (无参数):

    • 当你的装饰器逻辑非常简单,不需要任何外部配置,也无需维护任何状态时。
    • 例如:简单的日志打印、调试输出。
    • 优点:最简洁,易于理解和实现。
  • 带参数的函数装饰器:

    • 当你的装饰器行为需要根据一些配置参数来调整时。
    • 例如:权限验证需要指定角色,缓存装饰器需要指定过期时间。
    • 优点:提供了灵活性,无需修改装饰器本身即可改变其行为。
  • 类装饰器 (无参数或带参数):

    • 需要维护状态:当装饰器需要在多次函数调用之间保持一些信息时,例如一个函数调用计数器、一个简单的缓存或限流器。
    • 需要更复杂的配置和方法:当装饰器的逻辑比较复杂,你希望将其组织成一个类,利用类的属性和方法来管理其行为。
    • 利用面向对象特性:当你希望通过继承来扩展或修改装饰器行为时。
    • 优点:强大的组织能力,支持状态管理和面向对象的设计原则,适用于复杂场景。

实战建议与注意事项

  1. 始终使用 functools.wraps: 这可以帮助你保留被装饰函数的元信息(如 __name__, __doc__, __module__ 等),让调试和文档编写更加方便。在类装饰器中,对于无参数的类装饰器,可以在 __init__ 中使用 functools.wraps(func)(self);对于带参数的类装饰器,则在 __call__ 返回的 wrapper 函数上使用。
  2. 考虑异常处理: 装饰器应该能够优雅地处理被装饰函数可能抛出的异常,或者根据需要捕获并处理它们。
  3. 装饰器链: 多个装饰器可以叠加使用,它们会从内到外依次执行。理解其执行顺序对于调试非常重要。
    @decorator_outer
    @decorator_inner
    def my_func():
        pass
    # 等价于 my_func = decorator_outer(decorator_inner(my_func))
  4. 性能影响: 装饰器会引入额外的函数调用和逻辑,这在某些对性能要求极高的场景下可能会有轻微影响。但对于大多数应用而言,其带来的代码组织和复用性优势远大于这点性能损耗。
  5. 参数的清晰性: 确保你的装饰器参数命名清晰,自解释,以便其他开发者能够轻松理解其用途。

总结

Python 装饰器是 Python 编程中不可或缺的利器。从简单的日志记录,到复杂的权限控制和性能优化,熟练掌握函数装饰器(包括带参数的)和类装饰器,将极大地提升你的代码质量和开发效率。通过本文的深入学习和实战案例,相信你已经对 Python 装饰器有了“从入门到精通”的理解。现在,是时候在你的项目中实践这些强大的模式了!

正文完
 0
评论(没有评论)