想象你经营一家礼品包装作坊。每一位「函数」都是要发往用户手中的礼物,而装饰器就是那张万能的包装台:礼物放上去,裹纸、系带、交付,流程一气呵成。但客人们总是不满足——「用红色纸」「系金丝带」「附上贺卡」。于是,你需要一个三级流程:前台先记录客人的要求(外层参数),包装台根据要求选定工艺(中层接收函数),打包员最后动手完成增强(内层包装)。这就是带参数装饰器背后的三层嵌套结构。 再进一步,我们还能把整座包装台升级成一台「智能包装机」——那就是类装饰器。它不仅能包装,还能记住今天包了多少件、统计每件耗时,因为它自带「状态」和「记忆」。机器通过 __call__ 方法让自己随时可以开工。最后别忘了:包装完成后要贴回原产品的标签——functools.wraps 就是干这件事的,否则快递员看到的将是包装工的名字,而不是产品名。

岗位关联

装饰器是 Python 元编程的基石,也是 FastAPI、Flask 等框架的底层魔法。掌握带参数装饰器与类装饰器,等于拿到了编写高复用、高可维护代码的钥匙,让你的 AI 工程代码更优雅、更健壮。

目录
  1. 可视化演示
  2. 装饰器基础:认识包装台
  3. 三层嵌套结构:带参装饰器的核心
  4. 类装饰器实现:call 让机器开动
  5. functools.wraps:贴回原产品标签
  6. 常见踩坑点:车间事故单
  7. 实战应用场景:AI 流水线上的包装工
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

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

带参数装饰器:三层嵌套闭包结构 闭包变量捕获 @functools.wraps(func) \u2713 name 保留 Python带参数装饰器与类装饰器可视化:展示三层嵌套闭包结构、装饰器执行流程、call类装饰器状态管理及装饰器堆叠顺序。

装饰器基础:认识包装台

假设你的函数是作坊里准备寄出的手工香皂。没有装饰器之前,每块香皂都要在包裹上反复写地址、贴胶带——代码里每个函数都要重复粘贴相同的日志、计时逻辑。装饰器就是那张「包装台」:你只需把香皂放上去,机器自动完成包装。Python 装饰器的本质,是一个「接收函数、返回新函数」的可调用对象。

函数在 Python 中是一等公民,可以被当作参数传递,也能作为返回值返回。因此 @decorator 语法糖等价于 say_hello = decorator(say_hello)。原函数被传进去,新函数被换回来,调用方毫无感知,但函数行为已经悄悄升级。

写一个最简单的计时装饰器,需要定义一个 wrapper(*args, kwargs),在调用前后打点计时,最后 return wrapper。这里的 *args, kwargs 是为了保证能接收任意参数并透传给原函数,就像包装台能容纳各种形状的礼物。

有了包装台,每个函数都只专注自己的职责,作坊才能流水线协作。不过此刻包装台还只会一种样式,下一节我们要给它加上「定制参数」。

f∗=D(f),f∗(x)=f(x)+增强f^* = \mathcal{D}(f), \quad f^*(x) = f(x) + \text{增强}
装饰器 D 把原函数 f 包装成新函数 f*,调用 f* 时先执行增强逻辑,再执行原业务逻辑。
  1. $\mathcal{D}$ 装饰器包装台本身,一个可调用对象,把函数当原料吞进去又吐出来。
    D:f↦f∗\mathcal{D} : f \mapsto f^*
  2. $f$ 原函数香皂本体,业务逻辑所在,比如打招呼的函数。
    f(x)=原业务逻辑f(x) = \text{原业务逻辑}
  3. $f^*$ 包装后函数带包装纸的最终成品,依然可以被正常调用。
    f∗(x)=增强逻辑+f(x)f^*(x) = \text{增强逻辑} + f(x)

三层嵌套结构:带参装饰器的核心

客人今天说「用红色纸,系金丝带」。包装台必须根据要求定制工艺。Python 带参数装饰器就是为此而生的「需求定制流水线」。当看到 @log_with(level="INFO") 时,语法糖拆开其实有两步调用:先执行 log_with("INFO") 拿到真正的装饰器,再把这个装饰器应用到函数上。

因此,函数被「包装」的过程变成了三个层次,每一层各司其职:最外层接收自定义参数,中层接收被装饰函数,内层执行真正的增强逻辑并调用原函数。以日志装饰器为例,@log_with("INFO") 会依次触发:log_with("INFO") 返回 decorator,decorator(say_hello) 返回 wrapper,最终调用 wrapper("Alice") 时打印日志并执行 say_hello("Alice")。

这种结构虽然多包了两层,却换来了极大的灵活性:同一个带参装饰器传入不同参数,就能复用在日志、权限、缓存等各种横切逻辑上。它像工厂里的「订单 → 排产 → 打包」流程,每一层都只向上一层负责。

三层嵌套像包装工厂的「订单 → 排产 → 打包」流程,虽然看起来绕,但换来的是极致的可配置性。现在,我们把整个过程再升级成一台有记忆的机器。

D(a)(f)=g,g(x)=f(x)+h(a)\mathcal{D}(a)(f) = g, \quad g(x) = f(x) + h(a)
带参数装饰器 D(a) 先接收参数 a,返回的装饰器再接收函数 f,最终得到包装函数 g;g 调用时会叠加参数带来的增强 h(a)。
  1. $\mathcal{D}(a)$ 外层参数调用先定制包装需求,返回一个真正的装饰器。
    D(a)→decorator\mathcal{D}(a) \rightarrow \text{decorator}
  2. $(f)$ 中层接收函数装饰器把原函数 f 拿进车间,准备包装。
    decorator(f)=g\text{decorator}(f) = g
  3. $g(x)$ 内层增强调用最终 wrapper 被调用时,先做增强 h(a),再委托给原函数 f。
    g(x)=h(a)+f(x)g(x) = h(a) + f(x)

类装饰器实现:call 让机器开动

前面几节是手工包装台,这一节我们造一台「智能包装机」。机器有记忆:今天包了多少件、每件耗时多少、是否超时。用类实现装饰器,天然拥有这种「状态」,因为实例的属性可以在多次调用之间持久保存。

Python 中,任何对象只要实现了 __call__ 方法,就可以像函数一样被调用。类装饰器的原理正是如此:类接收被装饰函数生成一个实例,实例通过 __call__ 完成包装逻辑。每次调用 f(x) 时,Python 实际执行的是 f.__call__(x),这样实例内部的计数、缓存、状态都能持续累积。

类装饰器的基本结构非常清晰:__init__ 保存被装饰函数,__call__ 里编写增强逻辑。用 @CallCounter 装饰函数,等价于 f = CallCounter(f),之后每次调用 f 都会经过 __call__,同时 count 属性不断累加。比起函数装饰器,这种「智能机器」更容易组织和复用相关行为。

智能机器的价值在于「状态」。对 AI 工程师来说,统计推理次数、缓存结果、记录耗时都变得顺手拈来。机器包装完后,还差最后一步——贴上正确的产品标签。

C(f)=c,c(x)=c.__call__(x)\mathcal{C}(f) = c, \quad c(x) = c.\text{\_\_call\_\_}(x)
类 C 接收函数 f 后生成实例 c;实例被调用时自动进入 call 方法,完成增强并委派原函数。
  1. $\mathcal{C}(f)$ 类接收函数类构造器把函数存为属性,完成实例化,得到智能机器 c。
    c=C(f)c = \mathcal{C}(f)
  2. $c(x)$ 实例被调用Python 遇到实例调用时自动触发 __call__ 协议。
    c(x)≡c.__call__(x)c(x) \equiv c.\text{\_\_call\_\_}(x)
  3. $c.\text{count}$ 状态累积机器内存,每次调用更新,这是函数装饰器难以做到的。
    c.count←c.count+1c.\text{count} \gets c.\text{count} + 1

functools.wraps:贴回原产品标签

包装完成后,箱子外面本来要写「礼物名称:手工香皂」。但如果机器忘了贴原标签,快递员看到的会是「包装师傅小王」的名字,导致调度员找不到货。装饰器也有同样的烦恼:包装函数默认叫 wrapper,文档字符串也是空的,和原函数完全脱节,调试、文档工具全被误导。

functools.wraps 就是来解决这个问题的「贴标机」。它是一个内置辅助装饰器,内部调用 functools.update_wrapper(wrapper, func),把原函数的 __module__、__name__、__qualname__、__annotations__、__doc__ 等元信息复制到包装函数上,还会更新 __dict__。

使用方式极简:在定义 wrapper 时,给它加上一行 @functools.wraps(func)。从此以后,help()、自动文档生成器、日志记录器都能正确显示被装饰函数的原始信息。在 AI 框架里,被装饰函数的名字常出现在链路追踪和日志中,多这一步能省去大量排查烦恼。

包装再完美,标签也不能贴错。有了 functools.wraps,包装后的函数在日志、文档、调试器里依然「表里如一」。接下来看看车间里常发生的事故。

name(f∗)=name(f),doc(f∗)=doc(f)\text{name}(f^*) = \text{name}(f), \quad \text{doc}(f^*) = \text{doc}(f)
wraps 把原函数的名称和文档复制给包装函数,让语义信息不因包装而丢失。
  1. $\text{name}(f)$ 原函数名真正的产品名,比如 make_sentence。
    name(f)=make_sentence\text{name}(f) = \text{make\_sentence}
  2. $\text{name}(f^*)$ 包装后函数名未处理时是 wrapper,不利于错误定位。
    name(f∗)=wrapper\text{name}(f^*) = \text{wrapper}
  3. $\gets$ 复制动作wraps 将左侧原函数的元信息赋给右侧包装函数。
    f∗.name←f.name,f∗.doc←f.docf^*.\text{name} \gets f.\text{name},\quad f^*.\text{doc} \gets f.\text{doc}

常见踩坑点:车间事故单

新工人上岗最容易犯三类错:忘了装螺丝、参数塞错口、环节顺序乱。装饰器的世界里也有对应的事故单。第一类:带参装饰器少写一层。有人看到 @log_with(level="INFO") 觉得括号麻烦,直接 @log_with,把函数直接塞给了最外层。但最外层本来要接收的是参数,于是函数被当成 level 用,运行时神秘报错。

第二类:忘记返回 wrapper。内部明明定义了增强逻辑,却漏写 return wrapper,导致装饰器最后返回了 None。函数被替换成空对象,一调用就抛 TypeError: 'NoneType' object is not callable。

第三类:堆叠顺序颠倒。当多个装饰器叠加时,它们的执行顺序是从下往上、从内到外。@A @B 等价于 A(B(f)),离 def 最近的 B 先完成包装,A 再包一层。误以为 A 先执行,会让日志、权限的叠加逻辑完全错乱。

记住工厂流水线的顺序——材料先进前一道工序,包装自然从内到外。调试堆叠装饰器时,只要记住「离 def 最近者先包」,就能准确推断执行链。现在,把这些设备部署到真实的 AI 装配线上。

D1(D2(f))≠D2(D1(f))\mathcal{D}_1(\mathcal{D}_2(f)) \neq \mathcal{D}_2(\mathcal{D}_1(f))
两个装饰器叠加顺序不同,最终的包装结构不同,表现形式也不同。
  1. $\mathcal{D}_1(\mathcal{D}_2(f))$ 先内后外先由 D2 包裹 f,再由 D1 包裹结果。
    先D2,后D1\text{先} \mathcal{D}_2, \text{后} \mathcal{D}_1
  2. $\mathcal{D}_2(\mathcal{D}_1(f))$ 先外后内顺序互换,D1 先包 f,D2 再包外层。
    先D1,后D2\text{先} \mathcal{D}_1, \text{后} \mathcal{D}_2
  3. $\neq$ 不等价增强逻辑的叠加顺序不同,行为结果自然不同。
    顺序⇒不同的包装层次\text{顺序} \Rightarrow \text{不同的包装层次}

实战应用场景:AI 流水线上的包装工

前面学会了包装台、定制流、智能机器,现在把它们部署到真实的 AI 装配线上。训练、推理、网络请求都充满了横切关注点:权限校验、重试、缓存、日志。装饰器正是将这些杂活抽离出来的利器。

先说权限校验。在 FastAPI 或 Flask 中,用带参装饰器 @require_role("admin") 保护管理接口:外层接收角色名,中层接收视图函数,内层校验当前用户身份,不通过就抛异常,通过才继续执行。一个装饰器就能统一管理所有接口的访问策略。

再说重试机制。大模型 API 常常偶发超时,用 @retry(max_times=3, base_delay=1) 包装调用函数:内层捕获异常,按指数退避策略重试,达到上限后才向上抛错。RAG 链路里检索、生成、打分每个环节都可以挂上这道保险。

最后是缓存加速。@cache_result(ttl=300) 包装推理函数:内层先查缓存,命中就直接返回结果,没命中再调用原函数并把结果回填。对于重复 prompt 的在线推理服务,这样可以显著降低延迟和算力成本。

把包装车间连成智能流水线后,AI 工程里的日志、鉴权、重试、缓存都被「智能外衣」接走,核心模型代码保持干净整洁——这就是元编程的优雅所在。

Retry(f)(x)={f(x)成功重试失败\text{Retry}(f)(x) = \begin{cases} f(x) & \text{成功} \\ \text{重试} & \text{失败} \end{cases}
Retry 装饰器接收重试策略,包装原函数 f;调用成功则返回结果,失败则按策略重试,直到成功或耗尽次数。
  1. $\text{Retry}(f)$ 带参装饰器接收重试次数、退避系数等配置,返回包装函数。
    Retry(max_times,delay)=decorator\text{Retry}(\text{max\_times}, \text{delay}) = \text{decorator}
  2. $f(x)$ 原函数调用真正发起请求,比如调用大模型 API 的远程函数。
    f(x)=API 调用f(x) = \text{API 调用}
  3. $\begin{cases} \text{成功} & f(x) \\ \text{失败} & \text{重试} \end{cases}$ 分支判断成功立即返回响应;失败则等待退避后重试。
    成功→f(x),失败→重试\text{成功} \rightarrow f(x), \quad \text{失败} \rightarrow \text{重试}

AI 应用场景

零信任权限校验:AI 平台的安全闸门

阶段:模型服务部署与 API 网关

解决的问题:传统做法是在每个管理接口里手写权限判断,代码重复且容易遗漏。带参装饰器 @require_role("admin") 把鉴权逻辑集中到装饰器内部,外层接收角色参数、中层接收接口函数、内层统一校验 token 和权限,接口函数本身只剩下纯业务逻辑。

具体案例:某 AI 中台有 60 个管理接口,分别面向管理员、算法工程师、访客三种角色。用 @require_role("admin") 统一装饰后,新接口上线只需加上一行注解,权限配置一目了然,安全审计也便捷了。

大模型推理缓存:让重复请求秒回

阶段:在线推理服务与性能优化

解决的问题:同一批 prompt 被反复请求时,每次都走完整前向传播,GPU 成本高、响应慢。类装饰器 @cache_result(ttl=300) 利用实例状态保存缓存字典,缓存命中直接返回,未命中才调用原推理函数并回填结果。

具体案例:一个商用问答机器人上线后,发现前 200 个高频问题占了总请求量的 40%。给核心推理函数加上缓存装饰器后,P95 延迟从 1.2 秒降到 180 毫秒,GPU 使用率下降 25%。

指数退避重试:RAG 链路的稳定器

阶段:Agent 工作流与外部模型调用

解决的问题:LLM 服务偶发限流或超时,直接抛错会让用户请求失败。带参装饰器 @retry(max_times=3, base_delay=1, backoff=2) 捕获异常,按 1s、2s、4s 的退避节奏自动重试,重试耗尽后才向上抛异常。

具体案例:某 RAG 系统的 Agent 要依次调用 embedding、检索、生成三个模型。给三个调用函数分别挂上重试装饰器,系统的请求成功率从 92% 提升至 99.3%,用户几乎感知不到偶发抖动。

Python 演示

python45 行
import functoolsimport time def log_with(level="INFO"):    """带参数装饰器:为函数添加指定级别的日志"""    def decorator(func):        @functools.wraps(func)        def wrapper(*args, **kwargs):            print(f"[{level}] 开始执行 {func.__name__}")            start = time.time()            result = func(*args, **kwargs)            print(f"[{level}] {func.__name__} 完成,耗时 {time.time()-start:.3f}s")            return result        return wrapper    return decorator class CallCounter:    """类装饰器:统计函数调用次数并保存状态"""    def __init__(self, func):        functools.update_wrapper(self, func)        self.func = func        self.count = 0     def __call__(self, *args, **kwargs):        self.count += 1        return self.func(*args, **kwargs) @log_with(level="INFO")def train_step(model_name):    """模拟一次训练步骤"""    time.sleep(0.01)    return f"{model_name} 完成训练" @CallCounterdef query_llm(prompt):    """模拟一次大模型请求"""    return f"回答: {prompt}" if __name__ == "__main__":    train_step("bert-base")    train_step("gpt-4o")    query_llm("什么是装饰器?")    query_llm("什么是闭包?")    print(f"query_llm 一共被调用了 {query_llm.count} 次")    print(f"函数名保留: {train_step.__name__}, {query_llm.__name__}")
目的:演示带参数装饰器(日志级别)和类装饰器(调用计数)的写法,并验证 functools.wraps 对函数元信息的保留效果。
  1. def log_with(level="INFO"):最外层,先接收装饰器的自定义参数。
  2. def decorator(func):中层,接收被装饰的原函数。
  3. @functools.wraps(func)把原函数的名称和文档复制给 wrapper。
  4. def wrapper(*args, **kwargs):内层,真正执行增强逻辑。
  5. class CallCounter:类本身就是一个装饰器,接收函数生成实例。
  6. def __init__(self, func):构造器保存原函数并初始化计数状态。
  7. def __call__(self, *args, **kwargs):让实例可调用,每次调用累加计数。
  8. query_llm.count读取类装饰器保存的跨调用状态,这就是「智能机器」的记忆。

常见误区

误区我一直以为 `@log_with("INFO")` 只需要两层结构,外层直接接收函数和参数就行,为什么非要包三层?

正解装饰器语法 @log_with("INFO") 等价于 func = log_with("INFO")(func)。第一次调用 log_with("INFO") 要先把参数消费掉并返回一个装饰器,第二次调用才把函数传进装饰器。少写一层,参数和函数就会互相挡路,必然报错或行为异常。

为何常见:初学者习惯了无参装饰器的两层结构,看到括号里有参数就不自觉想合并成一层层嵌套,忽略了语法糖隐含了「先求值、后应用」的调用顺序。

误区我明明在装饰器内部定义了 wrapper 并且执行了逻辑,为什么函数调用时一直报「'NoneType' object is not callable」?

正解装饰器的核心契约是「接收函数,返回函数」。如果内部定义了 wrapper 却忘记 return wrapper,装饰器默认返回 None,原函数就被替换成了空对象。包装台只有把包好的成品交出来,客人才能正常使用。

为何常见:写函数时很容易把注意力放在 wrapper 内部的逻辑上,却忽略了 decorator 这一层必须显式 return,这是过程式思维对「返回契约」的常见漏看。

误区函数加了装饰器后照常运行,但日志里显示的函数名全是 wrapper,还以为代码里到处都是同名函数。

正解包装函数默认把 __name__、__doc__ 等元信息覆盖掉,给调试和文档工具造成误导。只要在 wrapper 上加 @functools.wraps(func),原函数的名称、文档、模块、注解都会复制过来,日志立刻恢复清晰。

为何常见:函数「运行正常」掩盖了元信息丢失的问题,只有等到排查日志、生成 API 文档时才暴露,于是很多人总在出问题时才发现当初少写了 wraps。

误区我以为 `@A @B` 是 A 先执行,然后 B 再执行,因为代码看起来 A 在上方。

正解装饰器的顺序是「从下往上,从内到外」。@A @B 等价于 A(B(f)),最靠近 def 的 B 先把 f 包起来,A 再包一层。执行时最外层的 A 先进入,然后才轮到 B。把握不住顺序,日志和权限的叠加就会颠倒。

为何常见:人的阅读习惯是从上往下,而装饰器应用顺序是从下往上,这种「视觉方向」与「执行方向」的冲突很容易诱导误判。

面试题

Q1. @decorator 语法糖展开后等价于哪一行?

单选题 · easy

  • A.f = decorator()
  • B.f = decorator(f)← 正确答案
  • C.f = decorator(decorator)
  • D.f = decorator(f())

答案:B。装饰器语法糖的本质是把原函数作为参数传入装饰器,并把返回值重新赋给函数名,即 f = decorator(f)。选项 A 少传了函数,C 把装饰器本身当参数,D 则提前执行了函数,都不符合装饰器的契约。

Q2. functools.wraps 最主要解决什么问题?

单选题 · easy

  • A.装饰器运行速度太慢
  • B.装饰后函数元信息丢失← 正确答案
  • C.装饰器无法接收参数
  • D.类装饰器无法共存

答案:B。包装函数默认叫 wrapper,文档和模块信息也会丢失,functools.wraps 会把原函数的 __name__、__doc__ 等元信息复制过来。它不负责性能,也不是装饰器传参或类装饰器兼容性的解决方案。

Q3. @log_with(level="INFO") 的三层嵌套中,最外层函数的作用是什么?

单选题 · mid

  • A.接收被装饰的函数本体
  • B.接收装饰器的配置参数← 正确答案
  • C.直接执行原函数逻辑
  • D.返回最终的业务结果

答案:B。带参装饰器 log_with(level) 的最外层先接收配置参数,返回一个真正的装饰器;中层才接收被装饰函数;内层执行增强逻辑。如果最外层接收的是函数,就无法传入 level="INFO" 这类参数了。

Q4. 类装饰器通过哪个方法让实例变得可调用?

单选题 · mid

  • A.__init__ 实例化方法
  • B.__call__ 可调用协议← 正确答案
  • C.__new__ 对象创建方法
  • D.__del__ 对象销毁方法

答案:B。Python 中只要实现了 __call__,实例就能像函数一样被调用。类装饰器把被装饰函数存在实例里,每次调用都进入 __call__。__init__ 负责初始化,__new__ 负责创建,__del__ 负责清理,都不是可调用的关键。

Q5. 如果装饰器内部定义了 wrapper 却忘记 return wrapper,会发生什么?

单选题 · mid

  • A.原函数不受任何影响
  • B.每次调用都抛出 SyntaxError
  • C.函数名会被替换为 None← 正确答案
  • D.装饰器自动补上返回值

答案:C。装饰器返回什么,函数名就变成什么。漏掉 return 时默认返回 None,调用时就会报「'NoneType' object is not callable」。这不是语法错误,而是运行时错误,调试时很容易被误导。

Q6. 类装饰器相对于函数装饰器最突出的优势是什么?

单选题 · mid

  • A.运行速度一定更快
  • B.可以保存跨调用状态← 正确答案
  • C.不需要任何返回值
  • D.自动处理所有异常

答案:B。类装饰器生成的实例有属性,可以在多次调用之间保存计数、缓存、配置等状态,而普通函数装饰器只能借助闭包变量,组织复杂状态时比较笨拙。速度、异常处理、返回值都不是类装饰器的天然优势。

Q7. 两个装饰器叠加 @A 和 @B(@A 在上),哪个装饰器先包裹原函数?

单选题 · hard

  • A.靠上的 A 先执行
  • B.靠近 def 的 B 先执行← 正确答案
  • C.两个装饰器同时执行
  • D.按函数名排序执行

答案:B。装饰器叠加等价于嵌套调用 A(B(f)),即最靠近 def 的装饰器先应用,再逐层向外包裹。阅读时从上往下看,实际执行却从下往上,这是装饰器叠加最经典的混淆点。

Q8. 下面哪些情况会导致装饰器行为异常?

多选题 · mid

  • A.带参装饰器漏写中间层← 正确答案
  • B.忘记 return wrapper← 正确答案
  • C.wrapper 不接收 *args, kwargs← 正确答案
  • D.使用了 @functools.wraps

答案:ABC。漏写中间层会让参数和函数层级错乱;不 return wrapper 会把函数变成 None;wrapper 不接收任意参数就无法透传原函数参数。而使用 functools.wraps 是正确习惯,不会导致异常。

Q9. functools.wraps 会复制原函数的哪些信息?

多选题 · mid

  • A.__name__ 函数名称← 正确答案
  • B.__doc__ 文档字符串← 正确答案
  • C.__module__ 所在模块← 正确答案
  • D.__code__ 字节码指令

答案:ABC。update_wrapper 默认赋值 __module__、__name__、__qualname__、__annotations__、__doc__,并更新 __dict__。它只复制元信息,不会复制函数体的字节码,因为包装函数运行的是自己的增强逻辑。

Q10. 关于类装饰器 @CallCounter 的正确说法有哪些?

多选题 · hard

  • A.__init__ 接收被装饰函数← 正确答案
  • B.__call__ 实现包装逻辑← 正确答案
  • C.实例属性可保存多次调用状态← 正确答案
  • D.装饰后函数不再需要返回值

答案:ABC。类装饰器执行 f = CallCounter(f),__init__ 接收原函数;之后每次调用 f(x) 都会进入 __call__,实例属性可以累计调用次数。装饰器依然要求 wrapper(即 __call__)返回原函数结果,否则调用方拿不到返回值。