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

12次阅读
没有评论

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

Python 是一种以其简洁性和强大功能而闻名的语言。在其众多高级特性中,装饰器(Decorators)无疑是最优雅和强大的特性之一。它们允许我们在不修改函数或类原始代码的情况下,动态地“装饰”它们,从而添加新的功能或修改其行为。从日志记录、性能分析到权限控制,装饰器在实际开发中无处不在。

本文将带领你从装饰器的基本概念入手,逐步深入探索带参数的装饰器以及如何使用类来实现装饰器,并辅以丰富的代码示例,助你从入门走向精通,将装饰器真正融入你的日常开发。

装饰器初探:什么是装饰器?

在深入研究带参数装饰器和类装饰器之前,我们首先需要理解装饰器的核心概念。简单来说,装饰器是一个函数,它接受一个函数作为参数,并返回一个新的函数(或可调用对象),新函数通常在原始函数执行前后添加额外的逻辑。Python 提供了 @ 语法糖,使得装饰器的使用非常直观。

让我们看一个最简单的装饰器示例:一个用于测量函数执行时间的装饰器。

import time

def timer(func):
    """一个简单的装饰器,用于测量函数执行时间。"""
    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
def long_running_function(n):
    """一个模拟耗时操作的函数。"""
    sum_val = 0
    for i in range(n):
        sum_val += i * i
    time.sleep(0.1) # 模拟 I / O 等待
    return sum_val

@timer
def another_function():
    time.sleep(0.2)
    print("Another function finished.")

# 调用被装饰的函数
result = long_running_function(1000000)
print(f"Long running function returned: {result}")

another_function()

在这个例子中:

  1. timer 是我们的装饰器函数。它接受一个函数 func 作为参数。
  2. timer 内部定义了一个 wrapper 函数。这个 wrapper 函数才是真正替代原始 func 执行的函数。
  3. wrapper 函数使用 *args**kwargs 来确保它可以接收任何数量和类型的参数,并将其传递给原始函数 func
  4. timer 返回 wrapper 函数。
  5. @timer 语法糖等价于 long_running_function = timer(long_running_function)。它将 long_running_function 函数传递给 timer,然后用 timer 返回的新函数(即 wrapper)替换掉 long_running_function 的定义。

通过这个例子,我们理解了装饰器的基本工作原理:在不改变原始函数代码的情况下,为其添加了计时功能。

注意: 在实际开发中,为了保留被装饰函数的元信息(如 __name__, __doc__ 等),我们通常会使用 functools.wraps 装饰器来包装 wrapper 函数。

import time
from functools import wraps

def timer(func):
    @wraps(func) # 使用 functools.wraps 保留原函数的元信息
    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
def example_function(a, b):
    """这是一个示例函数,计算 a +b。"""
    time.sleep(0.05)
    return a + b

print(example_function.__name__) # 输出: example_function
print(example_function.__doc__)  # 输出: 这是一个示例函数,计算 a +b。

进阶之路:带参数的装饰器

前面的装饰器是固定的,每次使用都执行相同的逻辑。但很多时候,我们希望能够在使用装饰器时传入一些配置参数,例如,一个日志装饰器可能希望指定日志级别,或者一个权限装饰器需要指定所需的角色。这就是“带参数的装饰器”派上用场的时候了。

带参数的装饰器实际上是一个“装饰器工厂函数”,它接受装饰器的参数,然后返回一个真正的装饰器函数。这个真正的装饰器函数再接受被装饰的函数,并返回 wrapper 函数。听起来有点绕?看代码就明白了。

from functools import wraps

def log_decorator(level):
    """一个带参数的装饰器,用于根据指定的级别记录函数调用。"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(f"[{level}] 调用函数: {func.__name__},参数: {args}, {kwargs}")
            result = func(*args, **kwargs)
            print(f"[{level}] 函数 {func.__name__} 执行完毕,返回值: {result}")
            return result
        return wrapper
    return decorator

@log_decorator(level="INFO")
def add(a, b):
    return a + b

@log_decorator(level="DEBUG")
def subtract(a, b):
    return a - b

@log_decorator(level="WARNING")
def divide(a, b):
    if b == 0:
        print(f"[WARNING] 尝试除以零!")
        return None
    return a / b

print("n--- 调用 add 函数 ---")
add(10, 5)

print("n--- 调用 subtract 函数 ---")
subtract(20, 7)

print("n--- 调用 divide 函数 ---")
divide(10, 2)
divide(10, 0)

解析带参数装饰器的工作原理:

  1. log_decorator(level="INFO"):当你写 @log_decorator(level="INFO") 时,Python 首先会调用 log_decorator 函数,并将 "INFO" 作为 level 参数传入。
  2. log_decorator 函数执行后,它会返回内部的 decorator 函数。
  3. 此时,@ 语法糖实际上变成了 @decorator。也就是说,add = decorator(add)
  4. decorator 函数接收 add 作为 func 参数,并返回 wrapper 函数。
  5. 最终,add 被替换为这个 wrapper 函数。

这个多层嵌套的结构(装饰器工厂 -> 装饰器 -> 包装器)是实现带参数装饰器的关键。

灵活应用:装饰器的多个参数与使用场景

带参数的装饰器可以接受任意数量的参数,这使得它们在多种场景下都非常有用。

示例:一个权限检查装饰器

from functools import wraps

def require_permission(permission_level):
    """
    一个权限检查装饰器,根据用户权限决定是否执行函数。假设有一个全局的用户权限字典。"""
    def decorator(func):
        @wraps(func)
        def wrapper(user_id, *args, **kwargs):
            # 这是一个简化的权限检查逻辑
            user_permissions = {
                1: "admin",
                2: "editor",
                3: "viewer"
            }
            if user_id not in user_permissions:
                print(f"错误:用户 {user_id} 不存在。")
                return None

            user_level = user_permissions[user_id]

            # 简单比较权限级别,实际应用中可能需要更复杂的权限树或角色管理
            permission_levels = {"viewer": 1, "editor": 2, "admin": 3}

            if permission_levels.get(user_level, 0) >= permission_levels.get(permission_level, 0):
                print(f"用户 {user_id} (权限: {user_level}) 拥有足够权限 ({permission_level})。")
                return func(user_id, *args, **kwargs)
            else:
                print(f"拒绝访问:用户 {user_id} (权限: {user_level}) 不具备'{permission_level}'权限。")
                return None
        return wrapper
    return decorator

@require_permission("admin")
def delete_data(user_id, item_id):
    print(f"用户 {user_id} 删除了数据项 {item_id}。")
    return True

@require_permission("editor")
def edit_document(user_id, doc_id, content):
    print(f"用户 {user_id} 编辑了文档 {doc_id},内容: {content[:20]}...")
    return True

@require_permission("viewer")
def view_report(user_id, report_name):
    print(f"用户 {user_id} 查看了报告 {report_name}。")
    return True

print("n--- 测试 delete_data ---")
delete_data(1, "item_ABC")  # 管理员
delete_data(2, "item_DEF")  # 编辑,权限不足

print("n--- 测试 edit_document ---")
edit_document(2, "doc_123", "新的文档内容...") # 编辑
edit_document(3, "doc_456", "查看者尝试编辑...") # 查看者,权限不足

print("n--- 测试 view_report ---")
view_report(3, "monthly_sales") # 查看者
view_report(1, "confidential_report") # 管理员 

其他常见的使用场景:

  • 缓存 (@cache): 可以指定缓存的过期时间 (TTL)。
  • 重试机制 (@retry): 可以指定重试次数、重试间隔。
  • 频率限制 (@rate_limit): 限制在一定时间内函数调用的次数。
  • 数据验证 (@validate): 验证传入参数的类型、范围等。
  • 数据库事务 (@transaction): 确保一组操作在一个事务中执行。

突破界限:使用类实现装饰器

到目前为止,我们都使用函数来实现装饰器。然而,在某些情况下,使用类来实现装饰器会更加清晰和强大,特别是当装饰器需要维护状态时。

要使用类实现装饰器,这个类需要具备两个关键方法:

  1. __init__(self, func):构造函数,在装饰器被应用到函数上时调用,通常接收被装饰的函数作为参数。
  2. __call__(self, *args, **kwargs):使类的实例像函数一样可以被调用。当被装饰的函数被调用时,实际上是调用了类的实例的 __call__ 方法。

让我们用类来实现一个简单的计数装饰器,统计函数被调用的次数:

from functools import wraps

class CallCounter:
    """一个类装饰器,用于统计函数被调用的次数。"""
    def __init__(self, func):
        wraps(func)(self) # 同样使用 wraps 来保留被装饰函数的元信息
        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 increment(x):
    return x + 1

@CallCounter
def decrement(x):
    return x - 1

print("n--- 调用 increment ---")
print(increment(10))
print(increment(20))
print(increment(30))

print("n--- 调用 decrement ---")
print(decrement(5))
print(decrement(10))

# 访问装饰器实例的状态
print(f"increment 函数总共调用了 {increment.count} 次。")
print(f"decrement 函数总共调用了 {decrement.count} 次。")

类装饰器的工作原理:

  1. 当你写 @CallCounter 时,Python 会创建一个 CallCounter 的实例,并将被装饰的函数(例如 increment)作为参数传递给 __init__ 方法。
    • increment = CallCounter(increment)
  2. 现在,increment 变量不再指向原始函数,而是指向 CallCounter 类的一个实例。
  3. 当你调用 increment(10) 时,实际上是调用了 CallCounter 实例的 __call__ 方法。

类装饰器相比函数装饰器的优势在于:它们可以更容易地维护状态(如 self.count),并且可以将相关的功能和数据封装在一个类中,提高代码的组织性。

类装饰器的进阶:带参数的类装饰器

将带参数的装饰器与类装饰器结合起来,可以创建功能更强大、更灵活的装饰器。这种情况下,类装饰器的实现方式会有所不同:

  1. __init__(self, *decorator_args, **decorator_kwargs):构造函数此时接收的是装饰器本身的参数,而不是被装饰的函数。
  2. __call__(self, func):这个方法现在负责接收被装饰的函数 func,并返回一个真正的 wrapper 函数(或可调用对象)。这个 wrapper 函数才是执行原始逻辑并添加额外功能的。

让我们用带参数的类装饰器来实现一个通用的重试机制:

import time
import random
from functools import wraps

class Retry:
    """
    一个带参数的类装饰器,用于重试函数。参数:
        attempts (int): 最大重试次数。delay (int): 每次重试之间的等待秒数。exceptions (tuple): 需要捕获并重试的异常类型。"""
    def __init__(self, attempts=3, delay=1, exceptions=(Exception,)):
        self.attempts = attempts
        self.delay = delay
        self.exceptions = exceptions

    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for i in range(1, self.attempts + 1):
                try:
                    return func(*args, **kwargs)
                except self.exceptions as e:
                    print(f"函数 {func.__name__} 失败 (第 {i}/{self.attempts} 次尝试 ): {e}")
                    if i < self.attempts:
                        time.sleep(self.delay)
            print(f"函数 {func.__name__} 在 {self.attempts} 次尝试后仍失败。")
            raise  # 重新抛出最后一次异常
        return wrapper

@Retry(attempts=5, delay=2, exceptions=(ConnectionError, TimeoutError))
def unstable_network_call(url):
    """模拟一个可能失败的网络请求。"""
    if random.random() < 0.6:  # 60% 的几率失败
        if random.random() < 0.5:
            raise ConnectionError(f"连接到 {url} 失败!")
        else:
            raise TimeoutError(f"请求 {url} 超时!")
    print(f"成功获取 {url} 的数据。")
    return "Data from" + url

@Retry(attempts=3, delay=1) # 使用默认异常类型 (Exception)
def might_fail_division(a, b):
    result = a / b
    print(f"{a} / {b} = {result}")
    return result

print("n--- 测试 unstable_network_call ---")
try:
    unstable_network_call("http://example.com/api/data")
except (ConnectionError, TimeoutError) as e:
    print(f"最终捕获到异常: {e}")

print("n--- 测试 might_fail_division ---")
try:
    might_fail_division(10, random.choice([0, 2])) # 有一半概率除以零
    might_fail_division(10, 2)
except Exception as e:
    print(f"最终捕获到异常: {e}")

带参数的类装饰器工作原理:

  1. 当你写 @Retry(attempts=5, delay=2, ...) 时,Python 首先调用 Retry 类的 __init__ 方法,并将 5, 2 等参数传递给它。
    • decorator_instance = Retry(attempts=5, delay=2, ...)
  2. __init__ 方法创建并初始化一个 Retry 类的实例,将这些参数存储在实例的属性中 (self.attempts, self.delay)。
  3. 现在,@ 语法糖会把这个 decorator_instance 实例作为装饰器来处理。也就是说,unstable_network_call = decorator_instance(unstable_network_call)
  4. Python 会调用 decorator_instance__call__ 方法,并将原始的 unstable_network_call 函数作为 func 参数传递给它。
  5. __call__ 方法内部定义并返回了 wrapper 函数,这个 wrapper 函数封装了重试逻辑,并利用了 self 存储的 attemptsdelay 参数。

这种模式的类装饰器非常强大,它允许我们定义一个具有配置参数的装饰器,并且这个装饰器可以维护自己的状态,同时又能灵活地包装任何函数。

装饰器实战:将所学付诸实践

至此,我们已经全面探索了 Python 装饰器,从最简单的无参数装饰器,到带参数的函数装饰器,再到功能强大的类装饰器及其带参数的变体。这些技术是 Python 编程中实现代码复用、关注点分离和增强功能的重要工具。

在实际项目中,装饰器无处不在:

  • Web 框架 (Flask/Django):路由定义 (@app.route()), 权限验证 (@login_required), 视图缓存 (@cache_page)。
  • ORM 库 (SQLAlchemy):事件监听,自定义类型。
  • 并发编程 :线程池 / 进程池管理,异步函数的包装。
  • API 开发 :限流、认证、请求 / 响应日志。

掌握了这些,你将能够:

  1. 提高代码复用性 :将通用逻辑抽象成装饰器,避免重复代码。
  2. 增强代码可读性 :通过 @ 语法糖,清晰地表达函数的额外行为。
  3. 实现关注点分离 :将业务逻辑与非业务逻辑(如日志、计时、权限)分离开来。

总结与展望

Python 装饰器是 Python 语言的一颗璀璨明珠。从函数装饰器的基本用法,到通过多层嵌套实现带参数装饰器,再到利用类实现具有状态的装饰器,以及最终结合两者的带参数类装饰器,我们看到了装饰器强大的表达能力和灵活性。

理解这些概念并加以实践,将极大提升你的 Python 编程能力,让你能够编写出更优雅、更健壮、更易于维护的代码。在你的日常开发中,不妨多多思考哪些通用逻辑可以通过装饰器来抽象,你会发现一片新天地。继续探索 functools 模块中的其他工具,比如 lru_cache,它们也是装饰器强大应用的最佳实践。

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