引言在 Python 开发中我们常常遇到这样的场景某个函数已经上线运行突然需求变更要在函数执行前后增加日志记录、权限检查或者性能统计。如果直接修改原函数内部代码不仅违背开闭原则还会让函数变得臃肿不堪。这时候Python 装饰器就像一把瑞士军刀可以在不修改原函数的前提下优雅地为其增添“装饰”。装饰器是 Python 的一大特色也是面试高频考点。很多人会用符号定义装饰器但对其底层原理却一知半解。本文将从闭包讲起逐步揭开装饰器的神秘面纱并通过三个实用案例带你掌握日志记录、权限校验和函数缓存的完整实现最后分享开发中容易踩的坑与最佳实践。核心概念从闭包到装饰器1. 一切皆对象函数也是对象Python 中函数是一等公民可以像普通变量一样被引用、当作参数传递、作为返回值返回。def greet(name): return fHello, {name} # 函数可以赋值给变量 f greet print(f(World)) # 输出 Hello, World # 函数可以作为参数 def call_func(func, arg): return func(arg) print(call_func(greet, Alice)) # 输出 Hello, Alice2. 闭包装饰器的基石闭包是指内部函数引用了外部函数的变量并且外部函数返回内部函数。闭包可以让函数“记住”当初创建时的环境。def outer(msg): # msg 是外部函数的局部变量 def inner(name): # inner 使用了外部变量 msg return f{msg}, {name} return inner # 返回内部函数对象 say_hi outer(Hi) print(say_hi(Tom)) # 输出 Hi, Tom print(say_hi(Jerry)) # 输出 Hi, Jerry这里inner函数就是一个闭包它携带了外部作用域中的msg。装饰器本质上就是一种特殊的闭包接收一个函数作为参数并返回一个增强后的新函数。3. 最简单的装饰器我们现在定义一个装饰器它可以在原函数执行前后打印日志而不修改原函数的代码。import functools def simple_logger(func): 一个简单的日志装饰器 functools.wraps(func) # 保留原函数的元信息 def wrapper(*args, **kwargs): print(f[LOG] 开始执行函数: {func.__name__}) result func(*args, **kwargs) print(f[LOG] 函数执行完毕: {func.__name__}) return result return wrapper simple_logger def add(a, b): 返回两数之和 return a b print(add(2, 3)) # 输出: # [LOG] 开始执行函数: add # [LOG] 函数执行完毕: add # 5 print(add.__name__) # 输出 add而不是 wrapper print(add.__doc__) # 输出 返回两数之和代码解析functools.wraps是一个非常重要的工具它将原函数的__name__、__doc__等元信息复制到wrapper上避免元数据丢失。建议编写装饰器时永远加上它。wrapper内部可以访问外部传入的func构成了闭包。使用simple_logger语法糖等价于add simple_logger(add)。实战案例三大场景一步到位场景一权限校验装饰器Web 开发中经常需要检查用户是否登录或者是否具有某个角色。传统的做法是在每个视图函数里写if not user: return 403这会导致大量重复代码。装饰器可以将权限逻辑抽离出来。import functools # 模拟当前用户信息实际项目中可从 request 或全局上下文获取 current_user { username: admin, role: admin } def require_role(role: str): 装饰器工厂根据入参生成对应权限的装饰器 def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): # 从全局上下文中获取用户角色 user_role current_user.get(role, guest) if user_role ! role: raise PermissionError(f需要 {role} 角色当前角色为 {user_role}) return func(*args, **kwargs) return wrapper return decorator # 普通用户试图访问管理员页面 require_role(admin) def admin_dashboard(): return 欢迎访问管理员仪表盘 try: print(admin_dashboard()) # 角色匹配输出: 欢迎访问管理员仪表盘 except PermissionError as e: print(e) # 修改用户角色测试权限不足的情况 current_user[role] user try: print(admin_dashboard()) except PermissionError as e: print(e) # 输出: 需要 admin 角色当前角色为 user这里使用了装饰器工厂模式。require_role本身不是装饰器而是一个返回装饰器的函数。通过require_role(admin)我们动态生成一个专门校验admin角色的装饰器。这种方式极大提高了灵活性你可以轻松创建require_role(editor)等不同权限的装饰器。场景二函数执行计时与日志混合装饰器有时我们需要对函数执行时间进行统计并记录详细的调用参数和返回值便于性能分析和故障排查。import functools import time import logging # 配置日志 logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) def performance_logger(func): 记录函数执行时间、参数和返回值的装饰器 functools.wraps(func) def wrapper(*args, **kwargs): start time.perf_counter() # 调用原函数 result func(*args, **kwargs) elapsed time.perf_counter() - start logging.info( f函数 {func.__name__} 执行耗时 {elapsed:.6f} 秒 f参数 args{args}, kwargs{kwargs}返回值{result} ) return result return wrapper performance_logger def fibonacci(n): 递归计算斐波那契数演示耗时统计 if n 1: return n return fibonacci(n - 1) fibonacci(n - 2) # 测试 print(fibonacci(10)) # 输出 55并打印详细日志由于使用了递归这个装饰器会在每次递归调用时都打印日志你可能不希望这样。我们可以改进一下只为最外层调用记录内部递归不装饰。通常的做法是编写一个非递归的包裹函数或者使用contextmanager但这已经超出本文范围。一个简单方案是将递归函数定义在wrapper内部但会丢失原函数的命名。更好的实践是使用functools.lru_cache等优化手段这正好引出下一个场景。场景三带过期时间的函数缓存装饰器对于计算密集型的纯函数我们常常需要缓存结果以提升性能。functools.lru_cache提供了基于 LRU 的内存缓存但它不支持设置过期时间。下面我们自己实现一个支持 TTLTime-To-Live的缓存装饰器。import functools import time def ttl_cache(ttl_seconds: int 60): 装饰器工厂返回一个支持 TTL 的缓存装饰器 def decorator(func): cache {} # 用于存储 {args_tuple: (result, timestamp)} functools.wraps(func) def wrapper(*args, **kwargs): # 构造缓存的键考虑关键字参数 key (args, tuple(sorted(kwargs.items()))) now time.time() # 检查缓存是否存在且未过期 if key in cache: result, timestamp cache[key] if now - timestamp ttl_seconds: print([CACHE HIT] 返回缓存结果) return result # 缓存未命中或已过期重新计算 result func(*args, **kwargs) cache[key] (result, now) print([CACHE MISS] 计算并缓存结果) return result # 提供清理缓存的方法可选 def clear_cache(): nonlocal cache cache.clear() print([CACHE] 所有缓存已清空) wrapper.clear_cache clear_cache return wrapper return decorator ttl_cache(ttl_seconds3) # 缓存 3 秒 def expensive_computation(a, b): 模拟耗时的计算 time.sleep(1) # 模拟耗时操作 return a ** 2 b ** 2 # 测试缓存效果 print(expensive_computation(3, 4)) # 第一次计算1秒后输出 25 print(expensive_computation(3, 4)) # 命中缓存立即输出 25 time.sleep(3) # 等待缓存过期 print(expensive_computation(3, 4)) # 过期重新计算1秒后输出 25 # 手动清空缓存 expensive_computation.clear_cache() print(expensive_computation(3, 4)) # 再次计算这个实现有以下亮点使用字典存储缓存键为(args, sorted_kwargs)的元组保证相同参数命中。每次访问检查时间戳过期自动重新计算。通过给wrapper绑定clear_cache方法方便外部手动控制缓存。装饰器工厂模式让我们可以灵活指定不同的 TTL 值。实际生产环境中对于更复杂的场景建议使用cachetools库或者配合 Redis 实现分布式缓存。常见问题与注意事项1. 装饰器叠加顺序多个装饰器可以叠加使用执行顺序是从下往上装饰从上往下执行。即靠近函数定义的装饰器先应用。例如outer inner def target(): pass # 等价于 target outer(inner(target))调用target()时先执行outer内部 wrapper 的前置代码再执行inner的前置代码然后执行原函数最后逆序执行后置代码。理解顺序对调试非常重要。2. 使用functools.wraps保留元信息如果没有wraps被装饰后的函数会丢失__name__、__doc__等属性导致调试困难某些依赖这些属性的框架如 Flask 的 view 函数会出现异常。因此请始终在 wrapper 上加上functools.wraps(func)。3. 装饰有参数函数的通用性使用*args, **kwargs接收任意参数可以保证装饰器通用但有时候你可能需要装饰器也处理一些特殊参数。如果装饰器需要解析原函数的参数签名可以使用inspect模块绑定参数。4. 装饰器应用于类方法时的self问题当装饰器装饰类方法时wrapper接收的第一个参数通常是self实例本身。确保wrapper能正确处理即可。如果要给所有实例方法批量添加装饰器也可以考虑配合元类或类装饰器。5. 性能开销装饰器会引入额外的函数调用层在性能敏感的循环中需要注意。不过对于绝大多数业务逻辑这种开销可以忽略不计。另外一些缓存装饰器反而能显著提升性能。总结装饰器是 Python 中实现横切关注点AOP的强大工具它基于闭包和高阶函数能够在保持原有函数纯粹性的同时灵活地添加日志、权限、缓存等附加功能。本文从基础闭包讲起逐步推导出装饰器的本质并提供了三个可直接运行的完整示例。在实际项目中你可以将这些思路进一步封装构建自己的装饰器工具库。掌握装饰器的原理后你将写出更简洁、更可复用的代码也能更深入地理解许多 Python 框架如 Flask、Django的路由、认证等实现机制。最后给出几点最佳实践建议永远使用functools.wraps保留元信息。需要参数化装饰器时使用装饰器工厂函数返回装饰器。复杂缓存需求优先使用标准库或成熟的第三方库如functools.lru_cache、cachetools等。在装饰器中避免修改原函数的调用签名和返回值类型除非这是明确的设计意图。为装饰器编写单元测试验证被装饰函数的正常行为和附加逻辑。希望这篇文章能帮助你彻底理解 Python 装饰器并在日常开发中游刃有余。如有疑问或更好的实践欢迎在评论区交流讨论