装饰器是 Python 最强大的语法特性之一。它允许在不修改原函数代码的前提下,为其注入额外的行为——日志记录、性能计时、权限校验、缓存、重试,这些横切关注点都可以通过一个 @ 符号干净地剥离出来。
但装饰器的学习曲线并不平坦。它的底层依赖闭包、一等函数和高阶函数三个概念,不理解这些前置知识,写出来的装饰器容易埋坑。
本文从闭包讲起,逐步深入到带参数装饰器、类装饰器和企业级实战场景。
一、前置知识:闭包与一等函数
Python 中函数是一等公民:可以赋值给变量、作为参数传递、作为返回值返回。
def greet(name): return f"Hello, {name}"say_hello = greet # 函数赋值给变量say_hello("World") # "Hello, World"
闭包是指一个函数记住了它被定义时所在的作用域,即使那个作用域已经不存在了:
def make_multiplier(n): def multiplier(x): return x * n # n 来自外层函数作用域 return multiplier # 返回内层函数times_3 = make_multiplier(3)times_3(10) # 30 —— 它"记住"了 n=3
multiplier 是一个闭包:它携带了 n=3 这个自由变量。装饰器的本质,就是"一个接收函数、返回增强了行为的闭包的高阶函数"。
二、最简单的装饰器
def log_call(func): def wrapper(*args, **kwargs): print(f"调用 {func.__name__}") return func(*args, **kwargs) return wrapperdef add(a, b): return a + badd = log_call(add) # 手动应用装饰器add(3, 5) # 输出: 调用 add → 返回 8
这里 log_call 就是一个最朴素的装饰器:它接收 add,返回一个在调用前后加了打印逻辑的新函数 wrapper。*args, **kwargs 保证装饰器能适配任意签名的函数。
三、@ 语法糖
手动 add = log_call(add) 不够直观。Python 提供了 @ 语法:
@log_calldef add(a, b): return a + b
@log_call 等价于 add = log_call(add),只是写在函数定义上方,可读性更好。多层装饰器的执行顺序是自下而上:
@decorator_a # 第二步执行@decorator_b # 第一步执行def func(): pass
等价于 func = decorator_a(decorator_b(func))。装饰器 b 先包裹原函数,装饰器 a 再包裹结果。
四、functools.wraps——容易被忽略的关键一步
上面的装饰器有一个隐蔽问题:
@log_calldef add(a, b): """返回两数之和。""" return a + bprint(add.__name__) # "wrapper",不是 "add"print(add.__doc__) # None,原始文档丢失了
装饰器把原函数的元信息(__name__、__doc__、__module__、参数签名)覆盖了。修复方式:
from functools import wrapsdef log_call(func): @wraps(func) # 复制原函数的元信息 def wrapper(*args, **kwargs): print(f"调用 {func.__name__}") return func(*args, **kwargs) return wrapper@log_calldef add(a, b): """返回两数之和。""" return a + bprint(add.__name__) # "add" ✅print(add.__doc__) # "返回两数之和。" ✅
所有自定义装饰器都应该加上 @wraps(func)。这不是可选项。缺少它会导致调试工具(如 pytest、inspect 模块)行为异常,在大型项目中追踪 bug 会非常痛苦。
五、带参数的装饰器
有时需要让装饰器本身接收参数,比如 @retry(max_attempts=3)。这需要在普通装饰器外面再包一层——变成三层嵌套:
from functools import wrapsimport timedef retry(max_attempts=3, delay=1.0): """重试装饰器:失败后等待 delay 秒重试,最多 max_attempts 次。""" def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for attempt in range(1, max_attempts + 1): try: return func(*args, **kwargs) except Exception as e: if attempt == max_attempts: raise print(f"第 {attempt} 次失败: {e},{delay}s 后重试...") time.sleep(delay) return None return wrapper return decorator@retry(max_attempts=3, delay=2.0)def fetch_data(url): ...
三层结构拆解:
- 最外层
retry(max_attempts, delay):接收装饰器参数,返回 decorator - 中间层
decorator(func):接收被装饰函数,返回 wrapper - 最内层
wrapper(*args, **kwargs):实际的包裹逻辑
记忆技巧:每多一层参数,就多一层函数嵌套。
六、类装饰器
当装饰器逻辑复杂到需要维护状态时,用类实现比三层嵌套函数更清晰:
class CountCalls: """统计函数被调用次数的装饰器。""" def __init__(self, func): wraps(func)(self) # 让实例继承原函数的元信息 self.count = 0 def __call__(self, *args, **kwargs): self.count += 1 print(f"{self.__wrapped__.__name__} 已被调用 {self.count} 次") return self.__wrapped__(*args, **kwargs)@CountCallsdef process(): ...process() # process 已被调用 1 次process() # process 已被调用 2 次
__init__ 在装饰时执行一次,__call__ 在每次调用时执行。类属性自然承载状态,不需要闭包捕获。self.__wrapped__ 是 @wraps 自动附加的原始函数引用。
七、实战场景
7.1 函数执行计时
import timefrom functools import wrapsdef 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}s") return result return wrapper
7.2 简单内存缓存(memoize)
def memoize(func): cache = {} @wraps(func) def wrapper(*args): if args not in cache: cache[args] = func(*args) return cache[args] return wrapper@memoizedef fibonacci(n): if n < 2: return n return fibonacci(n - 1) + fibonacci(n - 2)
这个例子同时展示了装饰器如何让原本指数级复杂度的递归降到线性——仅凭一个 @memoize。
7.3 权限校验
def require_role(role: str): def decorator(func): @wraps(func) def wrapper(user, *args, **kwargs): if user.role != role: raise PermissionError(f"需要 {role} 权限,当前为 {user.role}") return func(user, *args, **kwargs) return wrapper return decorator@require_role("admin")def delete_user(user, user_id: int): ...
7.4 注册表模式
装饰器的另一个经典用途是构建插件注册表——在被装饰时自动将函数注册到某个集合中:
handlers = {}def register(event_type: str): def decorator(func): handlers[event_type] = func return func # 注意:不包裹,直接返回原函数 return decorator@register("click")def handle_click(data): ...@register("submit")def handle_submit(data): ...event = "click"handlers[event](payload) # 动态分发
这类装饰器不需要修改函数行为,纯粹利用 @ 语法的声明式特点做元编程。
本文总结
装饰器的核心价值是关注点分离——把"这个函数做什么"和"附加的行为(日志、计时、校验)"拆开。善用装饰器,业务逻辑和基础设施代码各归其位,修改任一方都不会牵动另一方。
几条实践原则:
- 三层嵌套不要怕,对应"装饰器参数 → 原函数 → 调用参数"三层映射。
- 逻辑复杂时优先考虑类装饰器,不要硬撑着写五层嵌套函数。
- 装饰器不是魔法——它只是语法糖,本质是高阶函数调用。理解闭包就等于理解了装饰器。
你在项目中使用频率最高的自定义装饰器是哪种?欢迎留言讨论。