装饰器是 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 的元信息(如 name、doc)会变成 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
注意:类装饰器没有内部闭包,状态(如计数)可以直接存储在实例属性中,逻辑更清晰。@wraps 在 init 中通过 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 进阶的必经之路。