Python 装饰器高级用法:带参数、类装饰器与缓存

FreeGuideOnline 最新 2026-06-16

Python 装饰器高级用法:带参数、类装饰器与缓存

装饰器是 Python 中一种强大的语法糖,它允许你在不修改原函数代码的前提下,为函数添加额外的功能。掌握了基础装饰器的写法后,理解带参数的装饰器、类装饰器以及缓存装饰器,将帮助你写出更灵活、可复用的代码。本教程面向有一定基础但想深入装饰器用法的开发者,逐步拆解三种高级模式。

1. 回顾:最简装饰器

一个最基础的装饰器本质上是一个接受函数、返回新函数的高阶函数:

def simple_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"调用 {func.__name__} 之前")
        result = func(*args, **kwargs)
        print(f"调用 {func.__name__} 之后")
        return result
    return wrapper

@simple_decorator
def greet(name):
    print(f"你好,{name}")

greet("世界")

输出:

调用 greet 之前
你好,世界
调用 greet 之后

如果我们需要让装饰器本身接收参数,比如控制是否启用日志,就不能简单套用上面的模式了。

2. 带参数的装饰器 —— 再包装一层

带参数的装饰器本质上是一个返回装饰器的函数。我们必须在装饰器外面再定义一层函数,用来接收装饰器参数。

2.1 语法结构

def decorator_with_args(arg1, arg2):
    def actual_decorator(func):
        def wrapper(*args, **kwargs):
            # 可以使用 arg1, arg2
            print(f"参数: {arg1}, {arg2}")
            return func(*args, **kwargs)
        return wrapper
    return actual_decorator

使用时,@decorator_with_args(value1, value2) 会先调用 decorator_with_args(...),返回 actual_decorator,然后再用 actual_decorator 装饰目标函数。

2.2 实际案例:日志级别控制

让我们实现一个可以根据 level 参数决定日志行为的装饰器。

def log(level="INFO"):
    def decorator(func):
        def wrapper(*args, **kwargs):
            if level == "DEBUG":
                print(f"[DEBUG] 调用 {func.__name__},参数: {args}, {kwargs}")
            else:
                print(f"[{level}] 执行 {func.__name__}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@log(level="DEBUG")
def add(a, b):
    return a + b

@log(level="WARNING")
def subtract(a, b):
    return a - b

add(3, 5)
subtract(10, 4)

输出:

[DEBUG] 调用 add,参数: (3, 5), {}
[WARNING] 执行 subtract

2.3 使用 functools.wraps 保留元信息

多层包装会丢失原函数的 __name____doc__ 等属性。一定要用 functools.wraps 将内层 wrapper 的函数信息更新为原函数的。

修正后的版本:

from functools import wraps

def log(level="INFO"):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(f"[{level}] {func.__name__}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

wraps 是一个装饰器,它自身接收原函数作为参数,并完成元信息复制。在带参数的装饰器中,它应该直接装饰内层的 wrapper

3. 类装饰器 —— 用对象管理状态

装饰器不仅可以是函数,任何可调用对象都可以作为装饰器。类实现 __call__ 方法即可充当装饰器。类装饰器最大的优势是能够方便地维护状态,并且逻辑可以拆分成多个方法,结构更清晰。

3.1 基本类装饰器

class CountCalls:
    def __init__(self, func):
        self.func = func
        self.num_calls = 0

    def __call__(self, *args, **kwargs):
        self.num_calls += 1
        print(f"第 {self.num_calls} 次调用 {self.func.__name__}")
        return self.func(*args, **kwargs)

@CountCalls
def say_hello():
    print("Hello!")

say_hello()
say_hello()

输出:

第 1 次调用 say_hello
Hello!
第 2 次调用 say_hello
Hello!

3.2 带参数的类装饰器

要让类装饰器接收参数,只需改写 __init__ 方法,让它接收参数,并返回一个可调用对象(实例本身)作为实际的装饰器。装饰过程的调用顺序与函数装饰器类似。

class CountCallsWithLimit:
    def __init__(self, max_calls):
        self.max_calls = max_calls
        self.call_count = 0

    def __call__(self, func):
        self.func = func
        return self.wrapper_function

    def wrapper_function(self, *args, **kwargs):
        self.call_count += 1
        if self.call_count > self.max_calls:
            raise RuntimeError("调用次数超限!")
        print(f"调用 {self.func.__name__},次数 {self.call_count}/{self.max_calls}")
        return self.func(*args, **kwargs)

使用:

@CountCallsWithLimit(3)
def limited_hello():
    print("Hello")

limited_hello()
limited_hello()
limited_hello()
# limited_hello()  # 这里会抛出 RuntimeError

注意:为了正确保留原函数元信息,我们仍可在 wrapper_function 上使用 @wraps(func)(需要在 __call__ 内手动包裹)。

3.3 在类装饰器中正确保留原函数信息

可在 __call__ 内部对 wrapper 应用 wraps

from functools import wraps

class Retry:
    def __init__(self, max_retries=3):
        self.max_retries = max_retries

    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, self.max_retries + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"尝试 {attempt} 失败: {e}")
            raise Exception(f"{self.max_retries} 次重试后仍失败")
        return wrapper

@Retry(max_retries=2)
def unstable_func():
    print("执行中...")
    raise ValueError("连接失败")

unstable_func()

4. 实战:用装饰器实现缓存

缓存是装饰器的一个经典应用场景。我们可以将函数的输入参数和对应的返回值存储起来,下次遇到相同参数时直接返回缓存结果,避免重复计算。Python 标准库提供的 functools.lru_cache 就是现成的强大工具,但自己实现一个可以更深入地理解装饰器的工作方式。

4.1 简单字典缓存装饰器

def simple_cache(func):
    cache = {}
    @wraps(func)
    def wrapper(*args):
        if args in cache:
            print(f"缓存命中: {args}")
            return cache[args]
        result = func(*args)
        cache[args] = result
        return result
    return wrapper

@simple_cache
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(10))  # 第一次计算
print(fibonacci(10))  # 缓存命中

局限:该装饰器仅支持以位置参数作为缓存键,且不能处理关键字参数和不可哈希类型。生产环境中我们应使用 functools.lru_cache 或自定义更健壮的键生成机制。

4.2 利用 functools.lru_cache

from functools import lru_cache

@lru_cache(maxsize=128)
def compute_heavy(x, y):
    print(f"实际计算: {x} + {y}")
    return x + y

compute_heavy(3, 5)  # 实际计算
compute_heavy(3, 5)  # 不触发实际计算

lru_cache 支持:

  • maxsize:缓存最大条目数,设置为 None 时无限缓存。
  • typed:是否区分不同类型的相同值(如 33.0)。
  • 自动处理关键字参数并维护 LRU 淘汰策略。

4.3 支持过期时间的缓存装饰器

某些场景需要缓存具备时效性。我们可以用类装饰器结合时间戳实现。

import time
from functools import wraps

class TTL_Cache:
    def __init__(self, ttl_seconds=60):
        self.ttl = ttl_seconds
        self.storage = {}   # key -> (timestamp, result)

    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # 生成稳定的键(支持关键字参数)
            key = (args, tuple(sorted(kwargs.items())))
            if key in self.storage:
                ts, value = self.storage[key]
                if time.time() - ts < self.ttl:
                    return value
            result = func(*args, **kwargs)
            self.storage[key] = (time.time(), result)
            return result
        return wrapper

@TTL_Cache(5)
def get_data(source):
    print(f"从 {source} 获取数据...")
    return f"数据来自 {source}"

print(get_data("API"))   # 从 API 获取数据...
time.sleep(2)
print(get_data("API"))   # 返回缓存
time.sleep(4)
print(get_data("API"))   # 超过5秒,重新获取

5. 组合技巧与最佳实践

5.1 装饰器的执行顺序

当函数被多个装饰器装饰时,执行的顺序是从下往上(靠近函数定义的装饰器先执行),但调用时是从上往下。

@decorator_a
@decorator_b
def func():
    pass

等价于 func = decorator_a(decorator_b(func))

5.2 类装饰器 vs. 函数装饰器选择

  • 需要维护复杂状态或提供额外属性、方法时,优先使用类装饰器。
  • 功能简单、无需状态的装饰器,使用函数加 wraps 更为简洁。
  • 带参数的装饰器用函数实现也很直观,类装饰器在参数复杂时结构更清晰。

5.3 装饰器通用编写模板

无参数函数装饰器:

def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # 前置处理
        res = func(*args, **kwargs)
        # 后置处理
        return res
    return wrapper

带参数函数装饰器:

def decorator(param1, param2):
    def actual_decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # 可使用 param1, param2
            return func(*args, **kwargs)
        return wrapper
    return actual_decorator

带参数的类装饰器:

class DecoratorWithArgs:
    def __init__(self, param):
        self.param = param

    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # 可使用 self.param
            return func(*args, **kwargs)
        return wrapper

5.4 调试与元信息保护

无论哪种装饰器,只要内部定义了新函数,务必使用 @wraps(func) 来继承原函数的 __name____doc____module__ 等属性,否则会影响调试和文档自动生成。在类装饰器中,如果返回的是实例自身的可调用方法,请在该方法上应用 wraps

6. 小结与扩展

至此,你已经掌握了装饰器的三种进阶形态:

  • 带参数的装饰器:通过嵌套一层函数来接收参数,实现更灵活的行为控制。
  • 类装饰器:利用 __call__ 方法将类实例变为可调用装饰器,便于管理状态和逻辑拆分。
  • 缓存装饰器:从自制的简单字典缓存到标准库 lru_cache,再到带过期时间的自定义实现,极大提升了重复计算的性能。

装饰器还可以应用于类方法、类本身,甚至与描述器结合使用。如果你想进一步探索,可以尝试实现一个权限校验装饰器,或者一个自动重试的 TCP 连接装饰器。记住,理解装饰器本质(将函数/类作为参数并返回可调用对象)比死记硬背语法更重要。多动手实践,你很快就能设计出优雅且强大的装饰器。