装饰器是 Python 中非常实用且体现“一切皆对象”思想的特性。简单说,装饰器就是一个接受函数作为参数并返回新函数的可调用对象,它允许在不修改原函数代码的前提下,给函数增加额外的功能。
本质:一个函数包装另一个函数
装饰器的核心操作是“替换”。当你用 @decorator 修饰一个函数时,Python 实际上做了这件事:
# 使用装饰器语法
@decorator
def func():
pass
# 等价于手动调用
func = decorator(func)
也就是说,func 这个名字不再指向原来的函数,而是指向了 decorator(func) 的返回值。这个返回值通常是一个新函数(或任何可调用对象),它内部会调用原函数,并在其前后添加额外的逻辑。
实现原理:闭包 + 函数对象
绝大多数装饰器都是用闭包实现的。我们来看一个最基础的计时装饰器:
import time
def timer(func): # 1. 接受被装饰的函数
def wrapper(*args, **kwargs): # 2. 定义包装函数,使用闭包捕获 func
start = time.time()
result = func(*args, **kwargs) # 3. 调用原函数
end = time.time()
print(f"{func.__name__} 运行时间: {end - start:.4f} 秒")
return result
return wrapper # 4. 返回包装后的函数
@timer
def slow_function():
time.sleep(1)
print("完成")
slow_function()
# 输出:
# 完成
# slow_function 运行时间: 1.0002 秒
关键点解析:
timer是一个高阶函数,它接受函数func作为参数。wrapper是一个闭包,它“记住”了外层函数传进来的func,即使timer调用结束后,wrapper依然能够访问func。这就是闭包保存变量的机制。args, *kwargs确保包装后的函数可以接受任意参数,不影响原函数的调用方式。- 最后
timer返回wrapper,于是slow_function这个名称就指向了wrapper函数。
保持原函数的“身份”:functools.wraps
上面的装饰器存在一个隐藏问题:slow_function.name 会变成 "wrapper",它的文档字符串等元信息也会丢失。因为最终指向的是 wrapper,而不是原函数。这可能导致调试时产生困惑。
解决办法是使用 functools.wraps,它会把原函数的元信息复制到包装函数中:
from functools import wraps
def timer(func):
@wraps(func) # 保留 func 的 __name__, __doc__ 等
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} 运行时间: {end - start:.4f} 秒")
return result
return wrapper
现在 slow_function.name 依然为 "slow_function"。在生产环境编写装饰器时,加上 @wraps 是一个标准实践。
装饰器为什么能工作:函数是一等公民
装饰器能够实现,根本原因在于 Python 中函数是一等对象。这意味着:
- 函数可以被赋值给变量
- 函数可以作为参数传递给另一个函数
- 函数可以在另一个函数内部定义并返回
这些特性组合起来,就构成了装饰器的全部要素。
一个更实际的例子:权限检查
假设我们在写一个简单的 API,有些操作需要管理员权限:
def require_admin(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
if not user.get("is_admin"):
raise PermissionError("需要管理员权限")
return func(user, *args, **kwargs)
return wrapper
@require_admin
def delete_all_data(user):
print("所有数据已删除")
admin_user = {"name": "root", "is_admin": True}
normal_user = {"name": "guest", "is_admin": False}
delete_all_data(admin_user) # 正常执行
delete_all_data(normal_user) # 抛出 PermissionError
这里 require_admin 是一个典型的装饰器:它在调用 func 之前进行了一个权限判断,如果不符合条件就直接抛出异常,否则才执行真正的业务逻辑。这个检查逻辑与 delete_all_data 函数本身完全解耦,增删检查规则时都不需要改动业务函数。
总结:
- 本质:装饰器是一个把一个函数替换成另一个函数的语法糖。
- 原理:利用函数一等对象的特性,通过闭包捕获原函数,返回一个新的包装函数。
- 核心要点:接受函数、返回函数、用
@wraps保留元信息、args, *kwargs保持通用性。 - 实用之处:在日志记录、性能监控、权限验证、缓存、事务控制等横切关注点上,装饰器提供了极其优雅的复用方案。