装饰器与代理
装饰器是 Python 的招牌语法,也是代理模式的天然实现。面试主线:装饰器原理、lru_cache 缓存代理、代理模式的三种形态、和 Java AOP 的区别。
装饰器原理
装饰器本质是接收函数、返回函数的函数(闭包):
import functools
def logged(func):
@functools.wraps(func) # 保留原函数元信息
def wrapper(*args, **kwargs):
print(f"call {func.__name__}")
return func(*args, **kwargs)
return wrapper
@logged
def add(a, b):
return a + b要点:
@logged等价于add = logged(add),函数对象被替换为包装层functools.wraps复制__name__、__doc__等,否则调试和文档全乱- 带参装饰器(如
@cache(ttl=60))是装饰器工厂:外层函数返回装饰器 - 多个装饰器从下往上包装,执行时从上往下(洋葱)
lru_cache:缓存代理
from functools import lru_cache
@lru_cache(maxsize=128)
def get_user(user_id: int):
return db.query(user_id)- 按参数缓存返回值,命中直接返回(代理模式:替真实对象挡请求)
- 应用:昂贵计算、重复查询、FastAPI 依赖单例(见单例篇)
- 注意:参数必须可哈希;缓存的是返回值,对象可变时要小心
代理模式的三种形态
| 形态 | 意图 | Python 写法 |
|---|---|---|
| 虚拟代理 | 延迟创建昂贵对象 | 惰性属性(@property 首次访问才建) |
| 缓存代理 | 缓存重复结果 | lru_cache |
| 保护代理 | 控制访问权限 | 权限校验装饰器(@require_auth) |
| 远程代理 | 本地接口访问远端 | requests 客户端封装、RPC stub |
def require_auth(func):
@functools.wraps(func)
def wrapper(request, *args, **kwargs):
if not request.user:
raise PermissionError("未登录")
return func(request, *args, **kwargs)
return wrapper与 Java AOP 的对比
| 维度 | Python 装饰器 | Java AOP(Spring) |
|---|---|---|
| 织入方式 | 运行时直接包装函数 | 代理对象(JDK 动态代理/CGLIB) |
| 声明位置 | 函数上方装饰器语法 | 注解 + 切面配置 |
| 粒度 | 函数级,显式 | 切点表达式批量匹配(类/方法/包) |
| 横切能力 | 手动写 | 通知类型齐全(Before/After/环绕) |
| 反射依赖 | 无 | 依赖代理机制 |
共同点:都是在不改原代码的前提下增强行为(日志、鉴权、事务)。差异:Python 装饰器是显式逐函数标注,AOP 是声明式批量织入。Python 的横切可以用装饰器组合模拟,但没有 AOP 的切点表达式。
面试追问
- 装饰器实现原理? 闭包:外层接收函数返回包装函数。@ 语法就是赋值替换
- functools.wraps 为什么必要? 不复制元信息,包装后 name/doc 丢失,调试和文档受损
- lru_cache 是代理吗? 是缓存代理:参数命中直接返回缓存,不调用原函数
- 装饰器和 AOP 的区别? 装饰器显式逐函数、运行时包装;AOP 声明式批量织入、切点匹配。Python 没有 AOP 容器
- 多个装饰器的执行顺序? 从下往上包装,从上往下执行(洋葱模型)。装饰顺序影响执行顺序