想象你刚买了一部新手机。裸机手感很好、运行流畅,可你总担心一失手就摔出裂纹。于是你去手机壳店挑了一个壳——不是要改装手机,只是想给它加一层保护。套上壳后,打电话、刷视频、拍照,功能原封不动,但手机变得耐摔了、更好握了。如果这壳还预留了摄像头开口、印上了原型号标签,你会觉得它“就像本该长在手机上一样”。 Python 的装饰器就是给函数套这样的“手机壳”。函数体像手机的核心电路,而装饰器是一段包裹它的代码:不改动函数内部一行,却在调用前后“偷偷”附加日志、计时、权限检查等增强能力。它的底层密码有三件套:高阶函数(把函数当参数传来传去)、闭包(让内层函数记住外层变量)、functools.wraps(把原函数的“身份标签”复制到新壳上)。如果你正在搭建 AI 服务,你会频繁看到 @app.route、@login_required、@lru_cache 这些装饰器——理解它们,就等于看懂了工业级 Python 代码的半本秘籍。

岗位关联

掌握装饰器原理,你将从“能写函数”进阶到“能优雅地改造函数”——这是阅读和编写 AI 工程框架代码的必备素养,也是后端工程师区分初级与高级的标志之一。

目录
  1. 可视化演示
  2. 装饰器本质
  3. 闭包机制
  4. 元信息丢失
  5. functools.wraps
  6. 带参装饰器
  7. 多层装饰
  8. 工业应用
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

装饰器本质

小明新买了一部手机,出厂时是光秃秃的裸机。裸机完全可以正常打电话、刷视频,但他总担心手滑摔坏。他走进手机壳店,挑了一个壳套上去——手机还是那部手机,功能一点没变,只是更耐摔了。在 Python 世界里,函数体就像手机,装饰器就像手机壳:它包住函数,不动内部代码,却能给函数增加“抗摔”“美观”这类新特性。

为什么 Python 能做到“包住不动内部”?因为 Python 里函数是一等公民——函数可以像整数、字符串一样被传递:能放进列表、能当参数传进另一个函数,也能被另一个函数当作结果返回。这种“接收函数或返回函数”的函数叫高阶函数。装饰器本质上就是一个高阶函数:塞进去的是原函数,吐出来的是被包装过的新函数。

最常见的写法是用 @ 语法糖:

@ 只是一个语法缩写,让代码更可读。完整看:timer 接收 work 函数 → 构造一个新的 wrapper 函数 → 把 wrapper 重新赋给 work 这个名字。此后调用 work(),实际执行的是 wrapper(),而 wrapper 内部又调用原来的 work。

@decorator≡func=decorator(func)\text{@decorator} \equiv \text{func} = \text{decorator}(\text{func})

1. 语法糖标记:@ 写在函数定义上方,表示“把这函数交给装饰器处理”。

@decorator\text{@decorator}

2. 重新绑定:func = 表示原函数名最终被重新指向新函数。

func=\text{func} =

3. 函数调用:decorator(func) 把原函数作为参数传入,返回包装后的新函数。

decorator(func)\text{decorator}(\text{func})

手机壳店老板干的活和装饰器一模一样:你递给他一部裸机,他加工出一个套了壳的手机,再交到你手上——从此你手里这部手机,外壳变了,灵魂还是原来的灵魂。

python4 行
@timerdef work():    pass# 等价于 work = timer(work)

闭包机制

手机壳内部有一层缓冲棉,这层棉和你的手机轮廓“咬合”在一起,即使壳被拿远,棉依然记得手机的形状。Python 的闭包就是这层“记忆棉”:它让内层函数牢牢记住外层函数的局部变量,哪怕外层函数已经执行完毕。

装饰器的核心是“外函数包裹内函数”。外层函数执行时定义了内层函数,内层函数里用到了外层函数的局部变量——这种“内函数 + 被捕获的外层变量”的组合,就叫闭包。看这段代码:

outer 的局部变量 x 在 inner 中被引用。outer 执行完返回 inner 后,理论上 x 的栈帧应该被销毁,但 Python 发现 x 被 inner 捕获了,就把 x 一起打包进 inner 的闭包环境里。之后只要调用 inner(),x 就奇迹般地还在。

inner=outer(x),x is captured\text{inner} = \text{outer}(x), \quad x \text{ is captured}

1. 外层调用:outer(x) 带着参数 x 被执行,x 是外层局部变量。

outer(x)\text{outer}(x)

2. 返回内层:inner = 表示 outer 返回的是函数 inner,而不是普通数值。

inner=\text{inner} =

3. 捕获变量:x is captured 指 inner 引用了 x,x 被保存进 inner 的闭包环境。

x is captured\text{x is captured}

这就像手机壳的缓冲棉:它是在“套壳”这个动作发生时装进去的,但壳做好后,就算生产线把手机拿走,棉层依然记得手机的轮廓。装饰器里被捕获的外层变量通常就是原函数 func——wrapper 里随时能调用原函数,正是闭包在背后撑着。

python4 行
def outer(x):    def inner():      # 内层函数        return x + 1  # 引用外层变量 x    return inner      # 把内层函数返回出去

元信息丢失

你买了一部 iPhone 15,套上一个全包裹壳后,翻到手机背面,看到的不是 “iPhone 15“,而是壳上印的 ”shell-style v2“。手机还是那台手机,但”身份标识”看不到了。函数也有身份标识——__name__、__doc__ 这些内置属性。__name__ 是函数名,__doc__ 是函数开头的文档字符串。一旦被装饰器包装,这些属性就悄悄变了。

因为 greet 这个名字被重新绑定到 wrapper 上了,而 wrapper 是装饰器内部临时造出来的函数,它没有自己的名字和文档字符串。对调试来说这是灾难:异常堆栈里出现一串 wrapper,日志统计时根本分不清是哪个业务函数出了问题。

decorated.__name__=’wrapper’≠original.__name__\text{decorated}.\_\_name\_\_ = \text{'wrapper'} \neq \text{original}.\_\_name\_\_

1. 装饰结果:decorated 是被装饰后的函数对象。

decorated\text{decorated}

2. 名字属性:__name__ 是函数的“身份证姓名”属性。

.__name__\text{.\_\_name\_\_}

3. 标签替换:它变成了 wrapper——装饰器内部临时函数的默认名字。

= ’wrapper’\text{= 'wrapper'}

4. 身份错位:与原函数 original.__name__ 已经不同。

≠original.__name__\neq \text{original}.\_\_name\_\_

壳把型号标签盖住了,用户不知道这手机其实是 iPhone 15。如果再叠几层壳,每层都叫 wrapper,那就是手机被套了三层壳,每层都写着 "unknown model"——调试的人想找都找不到原机。

python7 行
@timerdef greet():    """向世界问好"""    print('hi') print(greet.__name__)  # wrapper,而不是 greet!print(greet.__doc__)   # None!

functools.wraps

好的手机壳会在摄像头、充电口、按键位置精准预留开口。用 functools.wraps 就相当于在壳上按原手机的位置开孔,让摄像头(__name__)和呼吸灯(__doc__)重新露出来。它是标准库给我们的“开孔工具”。

@wraps(func) 底层调用 functools.update_wrapper(wrapper, func)。update_wrapper 默认把 func 的 __name__、__doc__、__module__、__qualname__ 等复制到 wrapper,还会同步 __dict__。结果:greet.__name__ 恢复成 greet。

@wraps(func)≡update_wrapper(wrapper, func)\text{@wraps}(\text{func}) \equiv \text{update\_wrapper}(\text{wrapper},\ \text{func})

1. wraps 使用:@wraps(func) 装饰内层 wrapper,参数是原函数。

@wraps(func)\text{@wraps}(\text{func})

2. 等价实现:它本质上等价于调用 update_wrapper。

≡update_wrapper\equiv \text{update\_wrapper}

3. 复制方向:(wrapper, func) 表示把 func 的元信息复制给 wrapper,方向是从原函数到包装函数。

update_wrapper(wrapper, func)\text{update\_wrapper}(\text{wrapper},\ \text{func})

戴上“预留开口”的手机壳后,翻到背面依然能看见 "iPhone 15" 的型号名。用户完全不觉得这部手机被套过壳,用起来和裸机一模一样,但实际已经获得了防摔保护。

python7 行
from functools import wraps def timer(func):    @wraps(func)  # 把 func 的元信息复制给 wrapper    def wrapper(*args, **kwargs):        ...    return wrapper

带参装饰器

普通手机壳是“通版”,但有人想要印上自己名字的定制壳。通版壳工厂(装饰器)不够用,需要“定制壳工厂”——先接收“名字”这个参数,再产出对应的壳。有时候装饰器自己需要参数(比如重试次数、日志级别),这时 @timer 就变成 @timer(level='debug'),写法上多一层。

三层函数各司其职:repeat(times) 是工厂,接收配置参数,返回真正的装饰器;decorator(func) 接收原函数,返回 wrapper;wrapper 是最终代替原函数执行的新函数。注意 times 同样被闭包捕获,存在于每一层里。

repeat(times)→decorator(func)→wrapper\text{repeat}(\text{times}) \to \text{decorator}(\text{func}) \to \text{wrapper}

1. 工厂层:repeat(times) 接收配置参数 times。

repeat(times)\text{repeat}(\text{times})

2. 装饰器层:返回的 decorator 接收原函数 func。

decorator(func)\text{decorator}(\text{func})

3. 包装层:最终产出 wrapper,替代原函数执行。

wrapper\text{wrapper}

定制壳工厂的流水线是:你提交“印名字 XYZ”的需求 → 工厂按需生成版型 → 把手机套进版型。每次 @repeat(3) 都是一条定制流水线,3 就是夹在壳层里的定制信息。

python11 行
def repeat(times):          # 第一层:工厂    def decorator(func):    # 第二层:真正的装饰器        @wraps(func)        def wrapper(*args, **kwargs):  # 第三层:包装函数            for _ in range(times):                func(*args, **kwargs)        return wrapper    return decorator @repeat(3)def say(): ...

多层装饰

套了壳还要贴膜,贴了膜还要挂手机链。一层叠一层,每层各管一件事。一个函数可以同时被多个装饰器装饰,顺序和执行时机有讲究。

从下往上装饰:timer 先包住 work,log 再包住 timer(work)。调用时从外往里执行:log 先打日志 → 调用 timer 包装的函数 → timer 计时 → 调用真正的 work。所以口诀是:“装饰自下而上,执行自外向内”。

@log @timer≡work=log(timer(work))\text{@log}\ \text{@timer} \equiv \text{work} = \text{log}(\text{timer}(\text{work}))

1. 叠罗汉:@log 在上、@timer 在下,离函数最近的先被应用。

@log @timer\text{@log}\ \text{@timer}

2. 先包内层:timer(work) 先用计时壳包住原函数。

timer(work)\text{timer}(\text{work})

3. 再包外层:log(...) 再把日志壳包在最外面。

log(timer(work))\text{log}(\text{timer}(\text{work}))

4. 重绑定:work 最终指向最外层包装函数。

work=\text{work} =

贴膜后壳上落灰,链子又在膜外面摇晃;收拾时却得先摘链、再揭膜、最后才脱壳。执行顺序和装饰顺序正好相反。这提醒我们:叠加装饰器时,越通用的逻辑越要放在外层。

python4 行
@log@timerdef work(): ...# 等价于 work = log(timer(work))

工业应用

手机壳产业已经标准化:防摔、防水、磁吸,每款壳都留开口、标型号。工业级装饰器也一样,成体系的写法让“给函数加装备”变成流水线操作。真实系统中最常见的四类用途是日志记录、性能计时、权限校验、结果缓存。

  • 日志与鉴权:@login_required 包装 Web 视图函数,先验证登录态再执行业务;@check_quota 校验 API 配额。
  • 性能计时:@timer 包裹耗时接口,自动上报单次调用耗时,形成监控曲线。
  • 结果缓存:@lru_cache 和自定义 @cache 先查缓存,未命中才执行原函数,大幅降低重复计算。
  • 最佳实践:① 一律 @wraps(func);② wrapper 参数统一用 (*args, kwargs) 透传;③ 一个装饰器只做一件事;④ 装饰器内部保持小函数、内聚。

这样即使 100 个接口都加了装饰器,异常堆栈依然清晰可辨。

Tdecorated=Toriginal+O(1)T_{\text{decorated}} = T_{\text{original}} + O(1)

1. 总耗时:T_decorated 是加壳后每次调用的总耗时。

TdecoratedT_{\text{decorated}}

2. 原函数耗时:T_original 是函数本身纯粹的业务耗时。

ToriginalT_{\text{original}}

3. 恒定开销:O(1) 指壳层增加的日志、鉴权、缓存检查时间,是常量级。

O(1)O(1)

标准化壳厂出货的每一个壳都开孔、都标型号,于是生产线上的每一部手机都能既享受保护又保持身份。工程师遵循同样的规范,写出一个个“标准壳”,让横切逻辑以统一姿态嵌入业务函数。

AI 应用场景

Web 接口的“门禁壳”

阶段:后端服务开发与接口鉴权

解决的问题:传统写法每个接口函数开头都要复制粘贴 token 校验和配额扣减代码,改动校验逻辑要全局搜索替换。用装饰器把鉴权逻辑抽离成公共外壳,接口函数只写业务,校验逻辑统一升级。

具体案例:某 AI 推理平台有 50 个模型调用 API,每个接口都要验证 API Key 与配额。工程师写了一个 @check_quota 装饰器:先校验 Key → 检查配额 → 通过才调用原函数 → 扣减配额。新增接口只需一行 @check_quota,50 个接口的权限逻辑从 500 行重复代码压缩成一个装饰器文件。

模型调用链路的“监控壳”

阶段:模型服务监控与性能观测

解决的问题:直接在每个模型函数里手动加 time.time() 埋点容易遗漏、格式不统一,日志散落各处难以聚合。用 @monitor 装饰器包裹推理函数,自动上报耗时、显存占用、失败次数。

具体案例:CV 团队把 stable_diffusion 生成函数包了一层 @monitor('sd_generation'),每次调用的耗时和显存峰值自动推送到 Prometheus。配合 Grafana 仪表盘,一次模型升级的回归问题在 10 分钟内就被定位到。

LLM 结果的“缓存壳”

阶段:大模型应用优化与成本控制

解决的问题:相同 prompt 反复调用大模型,既浪费 token 又拖慢响应。传统做法是在业务代码里手写缓存判断,侵入性强。用 @llm_cache(ttl=3600) 装饰器:命中缓存直接返回,未命中调用原函数后写缓存。

具体案例:某 Agent 项目把高频的意图识别 prompt 包上缓存壳,缓存命中率约 62%,大模型调用成本下降近半,接口 P95 延迟从 3.2 秒降到 0.4 秒。业务函数本身零改动,只是加了一行装饰器。

Python 演示

python69 行
"""装饰器原理演示:闭包 + 高阶函数 + functools.wraps运行: python decorator_demo.py"""from functools import wrapsimport time  def timer_without_wraps(func):    """不带 @wraps 的计时装饰器(元信息会丢失)"""    def wrapper(*args, **kwargs):        start = time.perf_counter()        result = func(*args, **kwargs)        elapsed = time.perf_counter() - start        print("[timer] {:.4f}s".format(elapsed))        return result    return wrapper  def timer_with_wraps(func):    """带 @wraps 的计时装饰器(保留 __name__ / __doc__)"""    @wraps(func)    def wrapper(*args, **kwargs):        start = time.perf_counter()        result = func(*args, **kwargs)        elapsed = time.perf_counter() - start        print("[timer] {:.4f}s".format(elapsed))        return result    return wrapper  def repeat(times):    """带参装饰器工厂:让函数重复执行 times 次"""    def decorator(func):        @wraps(func)        def wrapper(*args, **kwargs):            for i in range(times):                print("  run {}/{}".format(i + 1, times))                func(*args, **kwargs)        return wrapper    return decorator  @timer_without_wrapsdef bad_greet():    """向世界问好(元信息会丢失)"""    print("hello world")  @timer_with_wraps@repeat(3)def good_greet():    """向世界问好(元信息被保留)"""    print("hello world")  def main():    print("== 不使用 @wraps ==")    bad_greet()    print("bad_greet.__name__ =", bad_greet.__name__)    print("bad_greet.__doc__ =", bad_greet.__doc__)    print()    print("== 使用 @wraps ==")    good_greet()    print("good_greet.__name__ =", good_greet.__name__)    print("good_greet.__doc__ =", good_greet.__doc__)  if __name__ == "__main__":    main()
目的:用一个可运行的小程序对比“不写 @wraps”与“写 @wraps”在函数身份上的差异,同时演示带参装饰器和多层装饰。
  1. def wrapper(*args, **kwargs):wrapper 是手机壳本体,*args, **kwargs 保证任何形状的手机都能套进去。
  2. @wraps(func)在壳上按原机型开孔,把 __name__ 和 __doc__ 复制到壳上。
  3. def repeat(times):定制壳工厂的第一层:先接收“3 次”这个参数。
  4. return wrapper工厂出货:把包装好的新函数交回给 Python 运行时。
  5. @timer_with_wraps @repeat(3)先贴膜再套壳;调用时先走计时壳,再走进门壳里的重复逻辑。
  6. bad_greet.__name__读一读壳上的标签,发现没开孔时标签悄悄变成了 wrapper。
  7. good_greet.__doc__有开孔的壳保留了原文档字符串“向世界问好(元信息被保留)”。

常见误区

误区我一直以为 @ 是装饰器的必要条件,没有 @ 就不能给函数加功能,所以看到框架源码里到处是 @ 才觉得它很神秘。

正解@ 只是语法糖,本质等价于 func = decorator(func)。你完全可以在函数定义完成后手动调用装饰器并重新赋值,效果一模一样。@ 让我们把“包装动作”写在函数头顶,只是更好读、更好维护。

为何常见:教程几乎都用 @ 教学,很少有人展示手动赋值的等价写法,于是把语法糖误认为机制本身。

误区反正功能正常,name 变成 wrapper 也就难看一点,不影响运行,懒得写 @wraps。

正解这会在调试堆栈、日志统计、文档生成、框架路由注册里引发连锁问题。所有 wrapper 都叫 wrapper 会让异常追踪完全失真;签名丢失还会让 IDE 提示和参数检查失效。@wraps 一行代码换来所有便利,是规范装饰器的底线。

为何常见:初学者只关注“调用结果是否正确”,看不到元信息丢失在工程规模下会被放大成事故。

误区@repeat(3) 不过是把 3 传给 repeat,repeat 内部直接用普通装饰器那套两层结构就行。

正解普通装饰器是两层:decorator 接收 func。一旦装饰器本身要参数,就变成三层:最外层工厂先接收 3,返回真正的装饰器;装饰器再接收 func。少一层就会把 3 当成 func 去调用,直接报 TypeError。

为何常见:语法糖把三层的调用链隐藏到一行 @ 后面,让人误以为内部结构没有变化。

误区装饰从上往下执行,所以 @b 先跑,然后才是 @a 包住它。

正解装饰时从下往上应用:@a 先包住原函数,@b 再包住 a 的结果,等价于 f = b(a(f))。但调用时从外向内执行:先跑 b 的逻辑,再跑 a 的逻辑,最后进原函数。先包进去的反而最后执行。

为何常见:阅读顺序是自上而下,人们习惯把“先看到的”当作“先发生的”,而这个机制恰恰相反。

误区只要能在一个函数里定义另一个函数,就算用了闭包,内层函数本身就是闭包。

正解闭包是“内层函数 + 被它捕获的外层自由变量”的组合体。如果内层函数没有引用外层任何变量,它只是普通嵌套函数,不构成闭包。捕获的变量会随内层函数一同存活,这正是装饰器里 wrapper 能记住 func 的原因。

为何常见:教学示例往往只展示嵌套的外形,缺少“有没有捕获变量”这个关键判断,学生就被外形带偏了。

面试题

Q1. 装饰器的本质是什么?

单选题 · easy

  • A.接收函数并返回新函数的高阶函数← 正确答案
  • B.常见的设计模式,只在 Web 框架中出现
  • C.基于继承来扩展类行为的某种机制
  • D.Python 内置的某个标准库模块

答案:A。装饰器本质是接收函数、返回新函数的高阶函数,就像手机壳工厂:输入裸机,输出带壳手机。B 错在它是通用语言特性;C 说的是继承机制;D 把编程模式和标准库混为一谈,functools 只是辅助工具。

Q2. @log 装饰函数 greet,以下哪个写法与之等价?

单选题 · easy

  • A.log 函数直接调用 greet() 并返回其结果
  • B.greet 被重新赋值为 log() 的调用结果
  • C.log 被重新赋值为 greet() 的调用结果
  • D.greet 被重新赋值为 log(greet) 的结果← 正确答案

答案:D。@log 等价于 greet = log(greet),把原始函数作为参数交给 log,把返回的新函数重新绑定到 greet 名字。A 少传了函数本身;B 把 log() 空调用当参数;C 把赋值方向搞反,把 greet() 的结果赋给了 log。

Q3. 不写 @wraps 时,以下哪个属性会变成 'wrapper'?

单选题 · mid

  • A.被装饰函数的 class 属性值
  • B.被装饰函数的 name 属性值← 正确答案
  • C.被装饰函数的 module 属性值
  • D.被装饰函数的 code 属性值

答案:B。name 记录函数名字。不写 @wraps 时,函数名被重新绑定到内部 wrapper,name 自然变成 wrapper。这就像手机壳盖住了型号标签。class、module 虽有影响但典型症状就是 name 变化,code 是字节码对象,不叫 wrapper。

Q4. 闭包中的“自由变量”指的是?

单选题 · mid

  • A.在函数外部用 global 声明的全局变量
  • B.在定义函数时没有赋初值的局部变量
  • C.内层函数引用但定义在外层的变量← 正确答案
  • D.在列表推导式中临时生成的循环变量

答案:C。自由变量指在内层函数中使用、定义却在外部函数作用域里的变量,就像手机壳夹层里的缓冲棉,属于外层但被内层记住。A 是全局变量,B 是未初始化变量,D 是循环变量,都不涉及跨作用域捕获。

Q5. functools.wraps(func) 的底层实现主要调用了什么?

单选题 · mid

  • A.调用 copy.deepcopy 拷贝整个函数对象
  • B.调用 setattr 修改原函数的函数名
  • C.调用 sys.settrace 追踪函数执行过程
  • D.调用 functools.update_wrapper 复制元信息← 正确答案

答案:D。@wraps(func) 底层是 functools.update_wrapper(wrapper, func),把 func 的 name、doc 等复制到 wrapper。这相当于按原机型给壳开孔。A 会拷贝对象但语义不对;B 只改单个属性且方向反了;D 是调试跟踪工具,与复制元信息无关。

Q6. 带参装饰器 @repeat(3) 的嵌套结构是?

单选题 · mid

  • A.三层嵌套:工厂返回真正的装饰器再返回包装函数← 正确答案
  • B.两层嵌套:decorator 只需包裹 wrapper
  • C.一层结构:直接重写原函数内部代码
  • D.四层结构:还要依赖 Python 元类机制

答案:A。带参装饰器必须有工厂层:repeat(times) 返回真正的 decorator,decorator(func) 再返回 wrapper。好比定制壳生产:先接订单参数,再生产版型,最后套手机。B 少了一层,拿参数的位置错了;C 改写了函数体;D 与元类无关。

Q7. 下面代码的装饰与执行顺序是?

单选题 · hard

  • A.先执行 b 后执行 a,调用时先执行 b 逻辑
  • B.先执行 b 后执行 a,调用时先执行 a 逻辑
  • C.先执行 a 后执行 b,调用时先执行 b 逻辑← 正确答案
  • D.先执行 a 后执行 b,调用时先执行 a 逻辑

答案:C。等价于 f = b(a(f))。装饰自下而上:a 先包住原函数,b 再包住 a 的结果;执行从外向内:先执行 b 的逻辑。A 装饰顺序错;B 执行顺序错;D 装饰与执行顺序都反了。类比:先贴膜(a)再套壳(b),使用时先碰壳外(b)。

Q8. 不写 @wraps 会带来哪些问题?

多选题 · mid

  • A.被装饰函数的名字全都变成 wrapper← 正确答案
  • B.函数文档字符串 doc 也会丢失为 None← 正确答案
  • C.原函数代码逻辑会被复制成两份
  • D.调试堆栈中难以辨认出原函数名字← 正确答案

答案:ABD。不写 @wraps 时,name 变成 wrapper、doc 变 None、堆栈里全是 wrapper 导致难辨原函数。C 错:装饰器并没有复制原函数代码,只是新建了包装函数,原函数体只存在一份。

Q9. 关于装饰器的最佳实践,以下正确的是?

多选题 · hard

  • A.wrapper 参数写成 args 和 *kwargs 透传← 正确答案
  • B.一个装饰器只聚焦于单一项横切逻辑← 正确答案
  • C.内部必须用全局变量来保存中间状态
  • D.处处使用 functools.wraps 保留元信息← 正确答案

答案:ABD。最佳实践包括透传参数、单一职责、使用 @wraps。C 错:状态应通过闭包捕获,而不是污染全局命名空间——这正是闭包“记忆棉”的价值所在。全局变量会让多个装饰器实例互相干扰。

Q10. 装饰器常见的工业用途有哪些?

多选题 · easy

  • A.为接口统一加日志记录与调用跟踪埋点← 正确答案
  • B.为耗时函数自动采集性能计时的数据← 正确答案
  • C.在 Web 框架中实现统一的权限校验门禁← 正确答案
  • D.修改 Python 解释器底层的实现源码

答案:ABC。日志、计时、鉴权是装饰器最经典的三大工业场景,加上缓存共四大类。D 属于直接修改解释器源码,和装饰器无关。这些用途的共同点都是“横切逻辑”:不修改业务函数,却能横穿所有函数统一附加能力。