人人都会AI编程

9.4 装饰器

更新时间:2026-07-12

装饰器是 Python 中最具代表性的高级特性之一。它允许你在不修改函数或类本身代码的情况下,为其增加额外的功能(如日志、计时、权限校验、缓存等)。装饰器的本质其实是对闭包高阶函数的巧妙运用。

装饰器的本质与实现原理

装饰器本质上是一个接受函数作为参数,并返回一个新函数的可调用对象(通常是函数,也可以是类)。它利用闭包机制,在返回的新函数中包裹原函数的调用,并附加额外逻辑。

一个最简的自定义装饰器

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

@my_decorator
def say_hello(name):
    print(f"Hello, {name}")

say_hello("Alice")

输出

调用 say_hello 前
Hello, Alice
调用 say_hello 后

@my_decorator 语法等价于 say_hello = my_decorator(say_hello)。原函数被替换成了 wrapper,但通过 wrapper 内部调用 func,原功能得以保留,同时增加了前后处理的逻辑。

functools.wraps 的作用与标准写法

上面的装饰器有一个小问题:say_hello 的元信息(如 namedoc)会变成 wrapper 的,这会给调试和文档生成带来困扰。

print(say_hello.__name__)  # 输出 wrapper,而不是 say_hello

解决办法是使用 functools.wraps,它会把原函数的元信息复制到 wrapper 上。

标准写法

from functools import wraps

def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"调用 {func.__name__} 前")
        return func(*args, **kwargs)
    return wrapper

现在 say_hello.name 就会正确显示为 'say_hello'任何装饰器都应该使用 @wraps 来保持元信息,这是生产代码的基本规范。

带参数的装饰器

有时我们希望装饰器本身可以接收参数,例如指定日志级别或重复次数。这就需要在装饰器外面再包装一层函数,用来接收参数并返回真正的装饰器。

示例:一个可配置输出前缀的装饰器

from functools import wraps

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

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

add(2, 3)  # 输出 [DEBUG] 执行 add

写法解析:@log(prefix="DEBUG") 首先调用 log(prefix="DEBUG") 返回 decorator 函数,然后 decorator(add) 再返回 wrapper三层函数结构是带参数装饰器的通用模式。

多层装饰器

可以在一个函数上叠加多个装饰器,执行顺序是由外向内装饰,由内向外执行

@decorator1
@decorator2
def func():
    pass

等价于 func = decorator1(decorator2(func))。即 decorator2 先装饰,decorator1 后装饰。调用 func() 时,会先经过 decorator1 包装的代码,再经过 decorator2 的,最后执行原函数。简单说:装饰时从下往上,执行时从上往下

类装饰器

除了函数,类也可以用作装饰器。类装饰器依赖 call 方法,使实例可以被像函数一样调用。它通常在需要维护状态的场景下更有优势。

示例:统计函数调用次数

from functools import wraps

class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0
        wraps(func)(self)   # 将原函数信息复制到实例上

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

@CountCalls
def greet():
    print("Hi!")

greet()  # 第1次调用 greet
greet()  # 第2次调用 greet

注意:类装饰器没有内部闭包,状态(如计数)可以直接存储在实例属性中,逻辑更清晰。@wrapsinit 中通过 wraps(func)(self) 调用来保留元信息。

实际应用场景

装饰器在日常开发和框架设计中用法极广:

  • 日志记录:自动打印函数调用信息。
  • 性能计时:计算函数运行时长。
  • 权限验证:在 Web 框架中,如 Flask 的 @login_required
  • 缓存:如 functools.lru_cache 缓存计算结果,避免重复计算。
  • 参数校验:检查参数类型或范围。
  • 事务处理:数据库操作的自动提交/回滚。
  • 注册回调:将函数注册到某个全局列表或路由器中。

计时装饰器实例

import time
from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"{func.__name__} 运行耗时: {end - start:.4f} 秒")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)

slow_function()  # 输出: slow_function 运行耗时: 1.0xxx 秒

装饰器让你能够以声明式的方式横向关注点分离(将日志、安全等非核心逻辑从业务函数中抽离),使得代码更简洁、复用性更高。掌握装饰器是 Python 进阶的必经之路。