装饰器是 Python 中最具特色且功能强大的特性之一。它允许我们在不修改原始函数代码的情况下,为函数添加额外的功能——比如日志记录、性能计时、缓存结果、权限验证等。装饰器本质上是一个接受函数作为参数并返回新函数的高阶函数,它遵循“开放封闭”原则,让代码更模块化、更易于复用。然而,很多开发者仅停留在“会使用”的阶段,对于带参数的装饰器、类装饰器以及如何正确保留函数元信息等问题仍感到困惑。本文将通过四个完整的实战案例,从原理到应用,彻底讲透 Python 装饰器。
装饰器的本质:理解函数是一等公民
在 Python 中,一切皆对象,函数也不例外。函数可以被赋值给变量、作为参数传递给另一个函数、甚至作为函数的返回值。装饰器正是利用了这一特性。一个简单的例子:
def say_hello():
return "Hello!"
# 函数可以赋值给变量
greeting = say_hello
print(greeting()) # 输出: Hello!
# 函数可以作为参数传递
def execute(func):
return func()
print(execute(say_hello)) # 输出: Hello!
装饰器就是在此基础上延伸出来的设计模式。它接受一个函数作为参数,在内部定义一个新函数,在新函数中添加额外的逻辑,然后返回这个新函数。这个新函数通常被称为包装函数,它会在适当的时间点调用原始函数。
最简单的装饰器:从闭包说起
让我们从一个最简单的装饰器开始——在函数调用前后打印日志:
def log_decorator(func):
def wrapper(*args, **kwargs):
print(f"调用函数: {func.__name__}")
result = func(*args, **kwargs)
print(f"函数 {func.__name__} 执行完毕")
return result
return wrapper
这里 wrapper 是一个闭包,它引用了外部函数的变量 func。当我们使用 @log_decorator 语法时,等价于:
@log_decorator
def add(a, b):
return a + b
# 等价于:
# add = log_decorator(add)
result = add(3, 5)
# 输出:
# 调用函数: add
# 函数 add 执行完毕
print(result) # 8
注意 wrapper 使用了 *args 和 **kwargs,这使得装饰器可以适用于任何参数的函数。装饰器的核心优势在于,我们可以在不改变 add 函数本身的情况下,为它增加日志功能,而且这个日志功能可以复用到任何其他函数上。
保留函数签名:functools.wraps 的使用
上面的装饰器存在一个隐藏问题:被装饰后的函数实际上变成了 wrapper,它的 __name__ 和 __doc__ 等元信息都丢失了。
print(add.__name__) # 输出: wrapper (而不是 'add')
print(add.__doc__) # 输出: None (原始函数的文档字符串丢失)
这在调试和文档生成时可能造成困扰。解决方案是使用 functools.wraps,它能将原始函数的元信息复制到包装函数上:
from functools import wraps
def log_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"调用函数: {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_decorator
def add(a, b):
"""返回两个数的和"""
return a + b
print(add.__name__) # 输出: add
print(add.__doc__) # 输出: 返回两个数的和
从现在开始,我们编写的装饰器都将使用 @wraps(func) 来保持函数签名。这是一个重要的最佳实践。
案例一:构建高性能缓存装饰器
在计算密集型或 I/O 密集型应用中,缓存是提升性能的常用手段。我们可以编写一个装饰器,将函数的计算结果缓存起来,下次用相同参数调用时直接返回缓存值,避免重复计算或请求。
下面实现一个基于字典的内存缓存装饰器:
from functools import wraps
def memoize(func):
"""缓存装饰器:将函数结果缓存在内存中"""
cache = {}
@wraps(func)
def wrapper(*args):
if args in cache:
print(f"从缓存中获取结果,参数: {args}")
return cache[args]
print(f"首次计算,参数: {args}")
result = func(*args)
cache[args] = result
return result
return wrapper
@memoize
def fibonacci(n):
"""计算第 n 个斐波那契数(递归版)"""
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# 测试
print(fibonacci(30)) # 首次计算需要一些时间
print(fibonacci(30)) # 第二次直接从缓存返回,几乎瞬间完成
注意这里使用 args 作为缓存键。如果函数有 kwargs,则需要更精细的键生成策略。Python 标准库 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)
elapsed = time.perf_counter() - start
print(f"{func.__name__} 执行耗时: {elapsed:.4f} 秒")
return result
return wrapper
@timer
def slow_operation(n):
"""模拟耗时操作"""
total = 0
for i in range(n):
total += i
return total
result = slow_operation(10_000_000)
# 输出: slow_operation 执行耗时: 0.XXXX 秒
使用 time.perf_counter() 比 time.time() 精度更高,且不受系统时间调整的影响,是计时场景的最佳选择。这个装饰器可以套用在任何需要监控的函数上,帮助快速定位性能瓶颈。
案例三:智能重试装饰器实现优雅容错
在网络请求、数据库操作等不稳定场景中,失败是常态。一个智能的重试装饰器能极大提升程序的健壮性。下面的实现支持最大重试次数、指数退避和指定可重试的异常类型:
import time
import random
from functools import wraps
def retry(max_attempts=3, delay=1, backoff=2, exceptions=(Exception,)):
"""
重试装饰器
:param max_attempts: 最大重试次数
:param delay: 初始延迟(秒)
:param backoff: 退避倍数
:param exceptions: 需要重试的异常类型元组
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
current_delay = delay
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
if attempt == max_attempts:
raise # 最后一次重试仍失败,抛出异常
print(f"{func.__name__} 第 {attempt} 次失败: {e},{current_delay:.1f}秒后重试...")
time.sleep(current_delay + random.uniform(0, 0.5))
current_delay *= backoff
return wrapper
return decorator
@retry(max_attempts=4, delay=0.5, backoff=2, exceptions=(ValueError, TimeoutError))
def unstable_request(endpoint):
"""模拟不稳定的网络请求"""
import random
if random.random() < 0.7: # 70% 概率失败
raise ValueError(f"请求 {endpoint} 失败")
return f"来自 {endpoint} 的响应数据"
# 使用
try:
result = unstable_request("/api/users")
print(result)
except ValueError:
print("请求最终失败")
这里展示的是带参数的装饰器——装饰器本身需要接受配置参数,因此多了一层函数嵌套。外层函数 retry 接收参数并返回真正的装饰器 decorator,decorator 再返回 wrapper。使用 @retry(max_attempts=4, delay=0.5) 实际上等价于 retry(max_attempts=4, delay=0.5)(func)。
指数退避策略让每次重试的等待时间递增,避免在服务恢复瞬间形成请求风暴。加入随机抖动(random.uniform)可以防止多个客户端同步重试。
案例四:权限校验与访问控制装饰器
在 Web 应用或 API 开发中,权限验证是一个高频需求。我们可以通过装饰器来检查用户是否有执行特定操作的权限:
from functools import wraps
# 模拟用户会话和权限存储
current_user = {"role": "admin", "name": "张三"}
def require_role(role):
"""权限校验装饰器:检查当前用户是否具有指定角色"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
if current_user.get("role") != role:
raise PermissionError(f"需要 {role} 角色,当前角色: {current_user.get('role')}")
return func(*args, **kwargs)
return wrapper
return decorator
@require_role("admin")
def delete_user(user_id):
"""删除用户(仅管理员可执行)"""
return f"用户 {user_id} 已删除"
@require_role("superuser")
def reset_system():
"""重置系统(仅超级用户可执行)"""
return "系统已重置"
# 测试
print(delete_user(42)) # 当前角色为 admin,删除成功
try:
print(reset_system()) # 抛出 PermissionError
except PermissionError as e:
print(f"权限错误: {e}")
这个装饰器可以在任何需要权限控制的函数上使用,如 Flask/FastAPI 路由、Django View 等。实际项目中,current_user 可能来自请求上下文或线程局部变量,但核心思路不变:装饰器负责检查权限,被装饰函数只关注业务逻辑。
进阶:类装饰器与装饰器工厂
除了函数装饰器,Python 还支持类装饰器。类装饰器使用对象来维护状态,比闭包更直观。以下是一个用类实现的计数装饰器,记录函数被调用的次数:
class CallCounter:
"""类装饰器:统计函数调用次数"""
def __init__(self, func):
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"{self.func.__name__} 已被调用 {self.count} 次")
return self.func(*args, **kwargs)
@CallCounter
def greet(name):
return f"你好,{name}"
print(greet("Alice"))
print(greet("Bob"))
# 输出:
# greet 已被调用 1 次
# 你好,Alice
# greet 已被调用 2 次
# 你好,Bob
类装饰器通过 __init__ 接收函数,通过 __call__ 实现包装逻辑。相比闭包,类装饰器在需要维护复杂状态(如多个计数器、缓存字典、配置等)时更加清晰。
此外,我们之前案例三和案例四中使用的是装饰器工厂模式——一个返回装饰器的函数。装饰器工厂让你能为装饰器传递配置参数,灵活性最高,也是实际项目中最常用的形式。
总结
本文从装饰器的基本原理出发,通过缓存、计时、重试、权限校验四个完整的实战案例,循序渐进地讲解了函数装饰器、带参数装饰器和类装饰器的写法和应用。关键要点回顾:
- 装饰器是接受函数、返回函数的高阶函数,符合“开放封闭”原则。
- 使用 functools.wraps 保留原始函数的元信息,避免调试混乱。
- 带参数的装饰器需要额外一层函数嵌套(装饰器工厂模式)。
- 类装饰器通过
__call__方法实现,适合维护复杂状态。
掌握装饰器后,你将能编写出更加优雅、可复用且易于维护的 Python 代码。现在,不妨打开编辑器,为你的项目中的重复逻辑封装几个装饰器,感受它带来的架构提升。

