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

82次阅读
没有评论

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

Python 装饰器是其语言中最强大、最优雅的特性之一,它允许开发者在不修改函数或类源代码的情况下,动态地增加或修改其功能。从日志记录、性能监控到权限验证和缓存,装饰器无处不在。掌握装饰器,尤其是带参数的装饰器和类装饰器,是成为一名高级 Python 开发者必经之路。

本文将带领你深入探索 Python 装饰器的奥秘,从基础概念入手,逐步进阶到如何编写带参数的装饰器,以及如何利用类来创建功能更强大的装饰器。通过丰富的实战示例,你将不仅理解装饰器的工作原理,更能学会在实际项目中灵活运用它们,将你的 Python 技能提升到一个新的高度。

Python 装饰器核心概念回顾

在深入带参数装饰器和类装饰器之前,我们先快速回顾一下 Python 装饰器的基础知识。

函数作为一等公民与闭包

Python 中,函数是一等公民,这意味着函数可以像其他任何数据类型(如整数、字符串)一样被赋值给变量、作为参数传递给另一个函数,或者从一个函数中返回。

def say_hello(name):
    return f"Hello, {name}!"

greeter = say_hello # 函数赋值给变量
print(greeter("Alice"))

闭包是嵌套函数的一种特殊形式。当一个内部函数引用了其外部函数作用域中的变量,并且该外部函数返回了内部函数,那么即使外部函数执行完毕,内部函数依然能访问和使用那些外部变量,这就是闭包。装饰器的核心正是基于闭包实现的。

def outer_function(msg):
    def inner_function(name):
        return f"{msg}, {name}!"
    return inner_function

hello_greeter = outer_function("Hello")
print(hello_greeter("Bob"))

hi_greeter = outer_function("Hi")
print(hi_greeter("Charlie"))

装饰器语法糖的本质

一个最简单的装饰器本质上就是一个接受函数作为参数,并返回一个新函数的函数。Python 提供了一个 @ 语法糖来简化装饰器的应用。

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Something is happening before the function is called.")
        result = func(*args, **kwargs)
        print("Something is happening after the function is called.")
        return result
    return wrapper

@my_decorator
def say_hello(name):
    print(f"Hello, {name}!")
    return f"Hello, {name} from original!"

# 实际上等同于 say_hello = my_decorator(say_hello)
result = say_hello("Alice")
print(result)

functools.wraps 的重要性

上述 wrapper 函数会“遮蔽”原始函数的元数据,比如 __name__, __doc__ 等。这在调试和内省时会带来困扰。functools.wraps 装饰器可以帮助我们解决这个问题,它会将原始函数的元数据复制到包装函数上。

import functools

def my_decorator_with_wraps(func):
    @functools.wraps(func) # 使用 functools.wraps
    def wrapper(*args, **kwargs):
        print(f"Calling function: {func.__name__}")
        result = func(*args, **kwargs)
        print(f"Finished calling: {func.__name__}")
        return result
    return wrapper

@my_decorator_with_wraps
def greet(name):
    """Greets the given name."""
    return f"Hello, {name}!"

print(greet.__name__)
print(greet.__doc__)
print(greet("David"))

现在 greet.__name__ 会正确地显示 greet,而不是 wrapper

进阶:带参数的装饰器

当你希望装饰器的行为可以根据传入的参数进行调整时,就需要使用带参数的装饰器。例如,一个日志装饰器可能需要知道日志级别,一个权限装饰器可能需要知道需要的角色。

实现原理:三层嵌套函数

带参数的装饰器比普通装饰器多了一个层级。它是一个函数,接受装饰器自身的参数,然后返回一个真正的装饰器。这个真正的装饰器再接收被装饰的函数,最后返回包装函数。

基本结构如下:

def decorator_factory(arg1, arg2): # 外层:接收装饰器参数
    def decorator(func):          # 中层:接收被装饰函数
        @functools.wraps(func)
        def wrapper(*args, **kwargs): # 内层:接收被装饰函数的参数
            # 使用 arg1, arg2 和 func, args, kwargs 执行逻辑
            print(f"Decorator args: {arg1}, {arg2}")
            result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

当使用 @decorator_factory(arg1, arg2) 语法时,Python 首先会调用 decorator_factory(arg1, arg2),它返回 decorator 函数。然后,这个 decorator 函数被用来装饰实际的函数。

实战示例一:带参数的权限检查装饰器

假设我们需要一个装饰器来检查用户是否具有特定的角色才能访问某个函数。

import functools

def requires_role(role):
    """一个带参数的装饰器,用于检查用户是否拥有指定角色。"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(user, *args, **kwargs):
            # 这里的 user 是被装饰函数 expected_user_data 的第一个参数
            if user.get("role") == role:
                print(f"User'{user['name']}'has role'{role}'. Access granted.")
                return func(user, *args, **kwargs)
            else:
                print(f"User'{user['name']}'does not have role'{role}'. Access denied.")
                return None # 或者抛出权限异常
        return wrapper
    return decorator

# 模拟用户数据
admin_user = {"name": "Alice", "role": "admin"}
guest_user = {"name": "Bob", "role": "guest"}
editor_user = {"name": "Charlie", "role": "editor"}

@requires_role('admin')
def delete_data(user, item_id):
    """只有管理员才能删除数据"""
    return f"{user['name']} deleted item {item_id}."

@requires_role('editor')
def publish_article(user, article_id):
    """只有编辑才能发布文章"""
    return f"{user['name']} published article {article_id}."

print(delete_data(admin_user, 101))
print(delete_data(guest_user, 102))
print(publish_article(editor_user, 201))
print(publish_article(admin_user, 202)) # 管理员不具备编辑权限 

在这个例子中,requires_role 接收 role 参数,并返回一个接受函数的 decorator。这个 decorator 内部的 wrapper 在调用原始函数前,检查传入 userrole 是否符合要求。

实战示例二:带参数的缓存装饰器

一个缓存装饰器可能需要设置缓存的超时时间,或者指定缓存键的生成策略。

import functools
import time

_cache = {} # 简单的内存缓存

def cache(timeout=60):
    """
    一个带参数的装饰器,用于缓存函数的返回结果。:param timeout: 缓存失效时间(秒)。0 表示永不失效。"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # 生成缓存键,这里简单地用函数名和参数组合
            cache_key = (func.__name__, args, frozenset(kwargs.items()))

            if cache_key in _cache:
                cached_time, cached_value = _cache[cache_key]
                if timeout == 0 or (time.time() - cached_time) < timeout:
                    print(f"Cache hit for {func.__name__} (key: {cache_key})")
                    return cached_value
                else:
                    print(f"Cache expired for {func.__name__} (key: {cache_key})")
                    del _cache[cache_key] # 缓存过期,删除

            print(f"Calculating result for {func.__name__} (key: {cache_key})")
            result = func(*args, **kwargs)
            _cache[cache_key] = (time.time(), result)
            return result
        return wrapper
    return decorator

@cache(timeout=5) # 缓存 5 秒
def fetch_data(item_id):
    """模拟从数据库或 API 获取数据"""
    time.sleep(2) # 模拟耗时操作
    return f"Data for item {item_id} fetched at {time.strftime('%H:%M:%S')}"

@cache(timeout=0) # 永不失效的缓存
def get_config(config_name):
    """获取配置信息"""
    time.sleep(1)
    return f"Config'{config_name}'loaded."

print("--- Testing fetch_data (5s timeout) ---")
print(fetch_data(1)) # 首次调用,计算
print(fetch_data(1)) # 缓存命中
time.sleep(6)
print(fetch_data(1)) # 缓存过期,重新计算

print("n--- Testing get_config (no timeout) ---")
print(get_config("database")) # 首次调用
print(get_config("database")) # 缓存命中
time.sleep(2)
print(get_config("database")) # 缓存命中 (永不失效)

这个 cache 装饰器通过 timeout 参数控制缓存的有效时间,演示了带参数装饰器在实际应用中的强大功能。

再进阶:类装饰器

虽然函数装饰器非常灵活,但在某些情况下,类装饰器可以提供更强大的功能,尤其当你需要装饰器维护状态、提供更复杂的配置逻辑或利用面向对象的特性时。

实现原理:将类实例作为装饰器

Python 的装饰器机制本质上是要求装饰器是一个“可调用对象”(callable)。除了函数,类的实例也可以是可调用对象,只要它实现了 __call__ 方法。

当我们用一个类来作为装饰器时,通常有两种方式:

  1. 类实例作为装饰器

    • @MyClass 语法会创建一个 MyClass 的实例,并将被装饰的函数作为参数传递给 MyClass__init__ 方法。
    • 这个实例必须实现 __call__ 方法,__call__ 方法将作为被装饰函数的替换。
      这允许类的实例在 __init__ 中接收被装饰函数,然后在 __call__ 中执行包装逻辑,并且可以在实例中存储状态。
  2. 类本身作为装饰器 (更少见,通常用于元类或更高级的场景):

    • 如果类本身直接用作装饰器 (@MyClass),那么 MyClass 必须有一个 __call__ 静态方法或类方法,或者 MyClass() 构造函数返回一个可调用对象。这种情况通常比较复杂,我们主要关注第一种更常见和直观的用法。

我们将主要关注第一种: 类实例作为装饰器

import functools

class MyClassDecorator:
    def __init__(self, func):
        # __init__ 接收被装饰的函数
        functools.wraps(func)(self) # 将 func 的元数据复制到 self 实例
        self._func = func
        print(f"Decorator initialized for function: {func.__name__}")

    def __call__(self, *args, **kwargs):
        # __call__ 方法在被装饰函数被调用时执行
        print(f"Before calling {self._func.__name__} from class decorator.")
        result = self._func(*args, **kwargs)
        print(f"After calling {self._func.__name__} from class decorator.")
        return result

@MyClassDecorator
def example_function(a, b):
    print(f"Executing example_function with {a} and {b}")
    return a + b

print(example_function(10, 20))
print(f"Function name: {example_function.__name__}")

注意这里 functools.wraps(func)(self) 的用法,它将 func 的元数据复制到 self 实例上,使得 example_function.__name__ 等属性能正确显示。

实战示例一:带状态的类装饰器 – 函数调用次数统计

函数装饰器通常是无状态的(每次应用装饰器都会创建新的闭包)。如果需要装饰器在多次调用中维护状态,类装饰器是一个非常好的选择。

import functools

class CallCounter:
    """一个类装饰器,用于统计被装饰函数的调用次数。"""
    def __init__(self, func):
        functools.wraps(func)(self)
        self._func = func
        self.call_count = 0
        print(f"CallCounter initialized for {func.__name__}")

    def __call__(self, *args, **kwargs):
        self.call_count += 1
        print(f"Function'{self._func.__name__}'called {self.call_count} time(s).")
        result = self._func(*args, **kwargs)
        return result

    def get_count(self):
        return self.call_count

@CallCounter
def greet_person(name):
    return f"Greetings, {name}!"

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

print(greet_person("Alice"))
print(greet_person("Bob"))
print(f"greet_person has been called {greet_person.get_count()} times.")

print(calculate_sum(5, 3))
print(calculate_sum(10, 20))
print(calculate_sum(1, 1))
print(f"calculate_sum has been called {calculate_sum.get_count()} times.")

CallCounter 类的实例在被创建时(即应用 @CallCounter 时),会初始化一个 call_count 属性。每次被装饰函数被调用时,实际上是调用了 CallCounter 实例的 __call__ 方法,该方法会递增 call_count。这样,每个被装饰的函数都拥有自己独立的计数器状态。

实战示例二:带参数的类装饰器 – 节流 / 限速器

结合带参数的装饰器和类装饰器的优势,我们可以创建一个既能维护状态又可以配置的装饰器。例如,一个节流装饰器,限制函数在一定时间内只能被调用特定次数。

import functools
import time
from collections import deque

class RateLimiter:
    """
    一个带参数的类装饰器,用于限制函数的调用频率。:param max_calls: 允许在 `period` 时间内调用的最大次数。:param period: 统计调用的时间窗口(秒)。"""
    def __init__(self, max_calls, period):
        self._max_calls = max_calls
        self._period = period
        self._call_times = deque() # 存储每次调用的时间戳

        # 返回一个真正的装饰器
        def decorator(func):
            functools.wraps(func)(self) # 将原始函数的元数据复制到此实例
            self._func = func
            return self # 返回 self 实例,因为它实现了__call__方法
        return decorator

    def __call__(self, *args, **kwargs):
        current_time = time.time()

        # 清理旧的调用记录
        while self._call_times and self._call_times[0] < current_time - self._period:
            self._call_times.popleft()

        # 检查是否超过限制
        if len(self._call_times) >= self._max_calls:
            wait_time = self._period - (current_time - self._call_times[0])
            print(f"Rate limit exceeded for'{self._func.__name__}'. Please wait {wait_time:.2f} seconds.")
            # 可以选择等待或直接返回 None/ 抛出异常
            time.sleep(wait_time) # 强制等待
            return self(*args, **kwargs) # 再次尝试调用,或根据需求修改逻辑
        else:
            self._call_times.append(current_time)
            print(f"Calling'{self._func.__name__}'. Calls left: {self._max_calls - len(self._call_times)}")
            return self._func(*args, **kwargs)

# 这样使用:@RateLimiter(max_calls=3, period=5) # 5 秒内最多调用 3 次
def process_request(request_id):
    print(f"Processing request {request_id}...")
    return f"Request {request_id} processed."

print("--- Testing RateLimiter (3 calls per 5 seconds) ---")
for i in range(1, 7):
    print(process_request(i))
    time.sleep(1) # 每次调用间隔 1 秒

print("n--- After a pause ---")
time.sleep(5) # 等待一段时间,让限流器重置
for i in range(7, 10):
    print(process_request(i))
    time.sleep(0.5)

在这个 RateLimiter 例子中,__init__ 方法接收 max_callsperiod 这两个装饰器参数,并返回一个真正的装饰器 decorator。这个 decorator 接收被装饰函数 func,将其保存到 _func 属性,并返回 self 实例。因此,RateLimiter 实例既保存了配置参数,又通过 _call_times 属性维护了调用历史状态,并在 __call__ 方法中执行限流逻辑。

选择合适的装饰器类型

  • 函数装饰器(无参数):适用于简单、无状态的函数增强,例如简单的日志、计时。
  • 带参数的函数装饰器 :当你需要配置装饰器的行为,并且装饰器本身不需要维护复杂的、长期存在的状态时。这是最常见的进阶用法。
  • 类装饰器(带 / 不带参数)
    • 当装饰器需要维护跨多次函数调用的状态时(如计数器、限流器)。
    • 当你希望利用面向对象编程的特性(如继承、更复杂的内部逻辑组织)来构建你的装饰器时。
    • 当装饰器本身需要具有多个方法或属性,而不仅仅是一个可调用对象时。

总的来说,从简单到复杂,如果函数装饰器能满足需求,就优先使用它;如果需要配置,就使用带参数的函数装饰器;如果需要状态管理或更复杂的面向对象结构,就考虑类装饰器。

总结与展望

通过本文的学习,你已经掌握了 Python 装饰器从入门到精通的关键知识点。我们回顾了装饰器的基础,深入探讨了如何编写带参数的函数装饰器,以及如何利用类来创建功能更强大、可维护状态的类装饰器。

装饰器是 Python 语言的精髓之一,它体现了“开闭原则”和“关注点分离”的设计思想,让你能够在不修改原有代码的基础上,灵活地扩展和修改函数或类的行为。无论是日志、缓存、权限管理还是性能监控,掌握了带参数装饰器和类装饰器,都将让你在日常开发中如虎添翼,写出更加优雅、高效和可维护的 Python 代码。现在,拿起你的键盘,将这些强大的工具应用到你的下一个项目中吧!

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