题目重点:解释 Python 装饰器的函数重绑定、闭包、参数转发、wraps 元数据、叠加顺序和实际用途,并指出包装器改变调用语义的风险。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- 以同步函数的调用计时装饰器作训练例;不把计时实现视为题目题面。
- 被装饰函数参数与返回值均原样转发;异步函数需改用可 await 的包装器。
解题路线
- 先说语法:`@dec` 放在函数定义前,效果等同定义函数后执行 `f = dec(f)`;decorator 接受 callable 并返回新 callable,也可以先由带参数工厂产生 decorator。
- 包装器闭包持有原函数,用 `*args, **kwargs` 转发,返回原结果;若计时或记录日志,`try/finally` 可确保异常时也记录结束,但异常应继续向外传递。
- 用 `functools.wraps(f)` 保留名称、文档、注解及 `__wrapped__` 入口,方便 introspection 和工具。多个装饰器如 `@A` 在 `@B` 上方,相当于 `f = A(B(f))`:B 先装饰,调用时 A 外层先运行。
- 说明好处是把鉴权、缓存、重试、日志等横切逻辑复用到多个函数;同时提醒缓存 key、权限上下文、异常、异步语义和装饰顺序必须设计清楚。
手动走一遍
训练例 `@log` 修饰 `load(id)` 后,`load(7)` 先进入 wrapper,再调用原始 load 并返回结果。若 `@auth` 在 `@cache` 之上,调用时先 auth 再 cache;调换次序可能让缓存命中绕开授权检查,因此顺序是行为的一部分。
规模与取舍
普通包装器每次调用通常增加 O(1) 分发开销,原函数复杂度不变;日志序列化、缓存查找或重试会各自引入额外时间与空间。叠加 D 层装饰器有 O(D) 的包装调用深度。
易错点与追问
容易失分的边界
- 认为 `@` 是特殊执行器,而不解释函数重新绑定。
- 忘记返回原函数结果、吞掉异常或丢掉参数/关键字参数。
- 不使用 wraps 导致函数签名与文档工具失真。
- 把同步 wrapper 用于 async 函数却不 await,或把鉴权和缓存顺序写反。
继续深挖
- 带参数装饰器为何有工厂、decorator、wrapper 三层?
- 如何支持 async def 并保留异常与取消语义?
- 叠加缓存与权限装饰器时怎样避免跨用户结果泄漏?
技术参考
- Python Glossary: decorator:技术参考:装饰器定义与 @ 语法。
- Python functools.wraps:技术参考:wrapper 元数据与 __wrapped__。
