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

6次阅读
没有评论

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

Python 作为一门以其简洁和强大著称的语言,其“装饰器”机制无疑是高级特性中最具魔力之一。它允许开发者在不修改原有函数或类定义的情况下,动态地增加或修改其功能。这不仅大大提升了代码的复用性和可维护性,也让程序设计变得更加优雅。

如果你已经对基础的 Python 装饰器有所了解,那么本文将带你深入探索装饰器的更高级用法:带参数装饰器 类装饰器。我们将从原理到实战,为你揭示它们在复杂应用场景中的强大威力,助你从装饰器“入门”走向“精通”。

装饰器初探:基础回顾

在深入探讨之前,我们先快速回顾一下基础的 Python 装饰器。装饰器本质上是一个函数,它接收一个函数作为参数,并返回一个新的函数(通常是包装过的原函数)。

def simple_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"正在调用函数: {func.__name__}")
        result = func(*args, **kwargs)
        print(f"函数 {func.__name__} 调用完毕,结果: {result}")
        return result
    return wrapper

@simple_decorator
def say_hello(name):
    return f"你好, {name}!"

# 调用被装饰的函数
print(say_hello("张三"))

这里,@simple_decoratorsay_hello = simple_decorator(say_hello) 的语法糖。wrapper 函数在执行 func 前后添加了额外的逻辑。然而,这种简单的装饰器有一个局限:它无法在装饰时接收额外的配置信息。这时,带参数装饰器就派上用场了。

为了让装饰器的元数据(如函数名、文档字符串)不被包装函数覆盖,我们应该使用 functools.wraps

import functools

def simple_decorator_with_wraps(func):
    @functools.wraps(func) # 保留原始函数元数据
    def wrapper(*args, **kwargs):
        print(f"正在调用函数: {func.__name__}")
        result = func(*args, **kwargs)
        print(f"函数 {func.__name__} 调用完毕,结果: {result}")
        return result
    return wrapper

@simple_decorator_with_wraps
def greet(name):
    """一个简单的问候函数"""
    return f"Hello, {name}!"

print(greet("Pythonista"))
print(f"函数名: {greet.__name__}")
print(f"文档字符串: {greet.__doc__}")

你会发现 greet.__name__greet.__doc__ 都正确地指向了原始函数的信息,这在调试和反射时非常重要。

核心进阶:带参数装饰器

为何需要带参数?

想象一下,你有一个日志装饰器,但你希望能够指定日志的级别(例如,DEBUG, INFO, WARNING),或者在重试机制中指定重试次数和间隔。这些信息需要在装饰器应用时就提供,而不是在被装饰函数调用时。这就是带参数装饰器的应用场景。

工作原理

带参数装饰器实际上是一个 装饰器工厂。它是一个函数,接收装饰器所需的参数,然后返回一个真正的装饰器函数。这个返回的装饰器函数再接收被装饰的函数作为参数,最终返回一个包装函数。

其结构通常是三层嵌套:

  1. 最外层函数 (Decorator Factory):接收装饰器参数。
  2. 中间层函数 (Actual Decorator):接收被装饰的函数。
  3. 最内层函数 (Wrapper):包装并执行被装饰函数的逻辑。
import functools
import time

def retry(attempts=3, delay=1): # 最外层:装饰器工厂,接收参数
    def decorator(func): # 中间层:真正的装饰器,接收被装饰函数
        @functools.wraps(func)
        def wrapper(*args, **kwargs): # 最内层:包装函数
            for i in range(attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"第 {i+1} 次尝试失败: {e}")
                    if i < attempts - 1:
                        time.sleep(delay)
            raise Exception(f"函数 {func.__name__} 在 {attempts} 次尝试后仍失败。")
        return wrapper
    return decorator

# 实战示例:带参数的重试装饰器
attempts = 2
delay_seconds = 0.5

@retry(attempts=attempts, delay=delay_seconds) # 在这里传入参数
def unstable_function():
    import random
    if random.random() < 0.7: # 70% 的概率失败
        raise ValueError("模拟网络请求失败")
    return "操作成功!"

print("n--- 测试重试装饰器 ---")
try:
    print(unstable_function())
except Exception as e:
    print(f"最终结果: {e}")

@retry(attempts=5, delay=0.1)
def critical_data_fetch():
    import random
    if random.random() < 0.9: # 90% 的概率失败
        raise ConnectionError("数据库连接超时")
    return "获取关键数据成功!"

print("n--- 测试关键数据获取 ---")
try:
    print(critical_data_fetch())
except Exception as e:
    print(f"最终结果: {e}")

retry(attempts=attempts, delay=delay_seconds) 这行中,retry 函数被调用,并返回了 decorator 函数。然后,@decorator 语法糖将 critical_data_fetch 传递给这个 decorator 函数,最终返回一个包装了重试逻辑的新函数。

进阶应用:类装饰器

除了使用函数来创建装饰器,Python 也允许我们使用类来创建装饰器,这称为 类装饰器

为何选择类?

类装饰器在以下场景中特别有用:

  1. 需要维护状态:如果你的装饰器需要存储一些信息(例如,函数被调用的次数、缓存结果),类实例可以很自然地存储这些状态。
  2. 更复杂的逻辑封装:当装饰器的逻辑变得复杂,涉及多个方法或属性时,使用类可以更好地组织代码。
  3. 统一接口:在某些设计模式中,类装饰器可以提供更统一的接口。

工作原理

要将一个类用作装饰器,该类需要实现 __init__ 方法和 __call__ 方法。

  1. __init__(self, func):当装饰器被应用时(即 @ClassName@ClassName()),如果类直接装饰函数(不带参数),__init__ 会接收被装饰的函数作为参数。如果类作为装饰器工厂(带参数),__init__ 接收装饰器参数。
  2. *`call(self, args, kwargs)`:当被装饰的函数被调用时,实际上是调用了类实例的 __call__ 方法。这个方法负责执行装饰逻辑并最终调用原始函数。

示例 1:无参数的类装饰器(统计函数调用次数)

import functools

class CallCounter:
    def __init__(self, func):
        @functools.wraps(func)
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"函数'{self.func.__name__}'已被调用 {self.count} 次。")
        return self.func(*args, **kwargs)

@CallCounter
def calculate_sum(a, b):
    return a + b

@CallCounter
def calculate_product(a, b):
    return a * b

print("n--- 测试无参数类装饰器 ---")
print(f"Sum: {calculate_sum(10, 20)}")
print(f"Sum: {calculate_sum(5, 5)}")
print(f"Product: {calculate_product(3, 4)}")
print(f"Product: {calculate_product(2, 2)}")
print(f"Product: {calculate_product(1, 1)}")

在这个例子中,@CallCounter 语法糖会创建一个 CallCounter 类的实例,并将 calculate_sumcalculate_product 函数传递给实例的 __init__ 方法。当 calculate_sum(10, 20) 被调用时,实际上是调用了 CallCounter 实例的 __call__ 方法。

示例 2:带参数的类装饰器(权限验证)

要创建带参数的类装饰器,我们需要在类的 __init__ 方法中接收装饰器参数,并在 __call__ 方法中处理被装饰函数的调用。这需要一个额外的工厂函数,或者更常见的做法是,在类的 __init__ 中只接收装饰器参数,然后让类实例本身成为装饰器。

import functools

class AuthRequired:
    def __init__(self, required_role='user'): # 装饰器工厂部分:接收参数
        self.required_role = required_role

    def __call__(self, func): # 真正的装饰器部分:接收被装饰函数
        @functools.wraps(func)
        def wrapper(user, *args, **kwargs): # 包装函数
            if hasattr(user, 'role') and user.role == self.required_role:
                print(f"用户'{user.name}'具有所需角色'{self.required_role}',允许访问。")
                return func(user, *args, **kwargs)
            else:
                raise PermissionError(f"用户'{user.name}'(角色: {getattr(user,'role',' 无 ')}) 没有所需角色'{self.required_role}'。")
        return wrapper

class User:
    def __init__(self, name, role):
        self.name = name
        self.role = role

@AuthRequired(required_role='admin') # 传入参数
def delete_critical_data(user, data_id):
    return f"管理员'{user.name}'删除了关键数据: {data_id}"

@AuthRequired(required_role='user')
def view_public_profile(user, profile_id):
    return f"用户'{user.name}'查看了公共档案: {profile_id}"

print("n--- 测试带参数类装饰器 ---")
admin_user = User("Alice", "admin")
regular_user = User("Bob", "user")
guest_user = User("Charlie", "guest")

try:
    print(delete_critical_data(admin_user, "DATA_123"))
except PermissionError as e:
    print(e)

try:
    print(delete_critical_data(regular_user, "DATA_456"))
except PermissionError as e:
    print(e)

try:
    print(view_public_profile(admin_user, "PROFILE_A"))
except PermissionError as e:
    print(e)

try:
    print(view_public_profile(guest_user, "PROFILE_B"))
except PermissionError as e:
    print(e)

在这个带参数的类装饰器中,@AuthRequired(required_role='admin') 首先会创建一个 AuthRequired 的实例,并将 required_role='admin' 传递给 __init__ 方法。然后,这个实例本身(可调用对象)会被用作真正的装饰器,即 delete_critical_data = auth_instance(delete_critical_data)

装饰器的实际应用与最佳实践

常见应用场景

  • 日志记录 (Logging):在函数执行前后打印日志,记录参数和返回值。
  • 权限 / 身份验证 (Authentication/Authorization):检查用户是否登录或是否具有特定权限。
  • 缓存 (Caching):缓存函数结果,避免重复计算。functools.lru_cache 就是一个很好的例子。
  • 性能监控 (Performance Monitoring):计算函数执行时间。
  • 输入校验 (Input Validation):在函数执行前校验输入参数。
  • 事务管理 (Transaction Management):数据库操作的事务提交与回滚。
  • 重试机制 (Retry Logic):如上例所示,在函数失败时自动重试。

装饰器链

可以对一个函数应用多个装饰器。装饰器从内到外(从下到上)依次执行,即最靠近函数的装饰器最先被执行。

@decorator_outer
@decorator_inner
def my_function():
    pass

等价于 my_function = decorator_outer(decorator_inner(my_function))

functools.wraps 的重要性

再次强调,始终使用 functools.wraps 来包装你的 wrapper 函数。它能将原始函数的 __name____doc____module__ 等元数据复制到包装函数上,这对调试、文档生成和依赖于函数元数据的工具(如 Flask 的路由装饰器)至关重要。

异常处理

在装饰器内部,合理地处理被装饰函数可能抛出的异常非常重要。你可以选择捕获异常并重新抛出,或者在装饰器内部进行一些补救措施。

性能考量

虽然装饰器非常方便,但过度使用或不当使用可能会引入额外的开销。每次函数调用都会经过装饰器的包装逻辑。对于性能敏感的代码,需要权衡利弊。

总结

Python 装饰器是其语言特性中一颗璀璨的明珠,它以优雅的方式实现了代码的扩展和修改,遵循了开放 / 封闭原则,大大提升了代码的模块化和可维护性。

通过本文,我们不仅回顾了基础装饰器的用法,更深入地探讨了 带参数装饰器 类装饰器 这两种高级形式。带参数装饰器允许你在装饰时配置装饰器的行为,而类装饰器则为你提供了管理状态和封装复杂逻辑的强大能力。掌握这些高级技巧,你将能够编写出更健壮、更灵活、更具表现力的 Python 代码。

现在,你已经掌握了 Python 装饰器从入门到精通的关键知识,是时候将这些强大的工具运用到你的实际项目中,让你的代码焕发新的生机!

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