装饰器:函数装饰器、类装饰器
一句话概述
装饰器(Decorator)是 Python 中"不修改原函数代码就能给函数增加新功能"的语法糖。本质上,装饰器就是一个接受函数作为参数、返回新函数的高阶函数——用 @decorator 语法写在被装饰函数上方,Python 会自动把函数传给装饰器并用返回值替换原函数。装饰器广泛应用于日志记录、性能计时、权限校验、缓存等场景。
💡 核心要点:①装饰器本质是func = decorator(func)的语法糖——接受函数,返回新函数 ②无参装饰器:外层函数接收 func,内层 wrapper 调用 func 并添加逻辑 ③有参装饰器:三层嵌套——外层接收参数,中层接收 func,内层是 wrapper ④类装饰器:实现__init__接收 func、__call__实现包装逻辑 ⑤functools.wraps保留原函数的元信息(__name__、__doc__)
教学与演示
一、装饰器是什么——给函数"穿衣服"
是什么:装饰器是一个可调用对象(函数或类),它接收一个函数作为参数,返回一个新的函数。用 @decorator_name 语法写在目标函数定义之前,等价于 target_func = decorator_name(target_func)。装饰器在不修改原函数源代码的前提下,在原函数调用前后插入额外的逻辑——比如日志、计时、鉴权。
大白话 装饰器就像给手机"套一个壳"——手机本身(原函数)没有任何改变,但套上壳之后多了新的功能(防摔、支架)。你不想要壳了,随时可以拿掉——手机还是那个手机。@decorator 就是"套壳"这个动作。函数装饰器是硅胶壳,类装饰器是翻盖壳——形式不同,目的一样。
为什么:装饰器解决了"横切关注点"(Cross-Cutting Concerns)问题。比如你想给 10 个函数加日志功能——不用装饰器的话,你要在每个函数里都写 print(f"调用 {func_name}"),代码重复且难以维护。用装饰器,一行 @log 搞定。装饰器遵循"单一职责原则"——函数只做自己的核心逻辑,附加功能由装饰器统一管理。
大白话 就像机场安检——你不需要在每个登机口安排专门的安检员(在每个函数里写重复代码),而是设一个统一的安检通道(装饰器),所有人(函数调用)经过时自动被安检(执行附加逻辑)。如果安检规则变了(修改装饰器),所有登机口自动更新——只需改一处。
怎么做:
import numpy as np
import time
from functools import wraps
# ====== 1. 理解装饰器的本质——就是函数嵌套 ======
# @decorator 等价于 func = decorator(func)
def simple_decorator(func):
"""最简单的装饰器——接收函数,返回包装函数"""
def wrapper():
print(f" [装饰器] 函数 {func.__name__} 被调用了")
result = func() # 调用原函数
print(f" [装饰器] 函数 {func.__name__} 执行完毕")
return result
return wrapper # 返回包装后的新函数
def hello():
"""原函数——说 hello"""
print(" Hello, World!")
# 不使用 @ 语法——手动应用装饰器
print("=== 手动应用装饰器 ===")
hello_decorated = simple_decorator(hello) # 手动包装
hello_decorated() # 现在调用的是包装后的函数
# 使用 @ 语法——Python 自动完成上面的包装
@simple_decorator
def hi():
"""原函数——说 hi"""
print(" Hi, Python!")
print(f"\n=== @ 语法糖 ===")
hi() # 和手动包装效果完全一样
# ====== 2. 装饰器处理参数——用 *args, **kwargs 转发 ======
def timer(func):
"""计时装饰器——测量函数执行时间"""
@wraps(func) # 保留原函数的 __name__、__doc__ 等
def wrapper(*args, **kwargs):
"""包装函数——接收任意参数并转发给原函数"""
start = time.time()
result = func(*args, **kwargs) # 把所有参数原样转发
elapsed = time.time() - start
print(f" {func.__name__} 耗时: {elapsed:.4f} 秒")
return result
return wrapper
@timer
def compute_heavy(n):
"""模拟一个耗时计算——计算大数组的统计信息"""
data = np.random.randn(n, n)
eigenvalues = np.linalg.eigvals(data) # 计算特征值(O(n³))
return np.mean(np.abs(eigenvalues))
print(f"\n=== 计时装饰器 ===")
result = compute_heavy(200) # 200x200 矩阵
print(f" 计算结果(平均特征值绝对值): {result:.4f}")
print(f" 原函数名: {compute_heavy.__name__}") # 有 @wraps → 'compute_heavy'
# ====== 3. AI 场景:缓存装饰器(Memoization) ======
def memoize(func):
"""缓存装饰器——相同输入不重复计算,直接返回缓存结果"""
cache = {} # 闭包变量:存储已计算的结果
@wraps(func)
def wrapper(*args, **kwargs):
key = (args, tuple(sorted(kwargs.items())))
if key not in cache:
print(f" [计算] {func.__name__}{args} → 缓存未命中,开始计算...")
cache[key] = func(*args, **kwargs)
else:
print(f" [缓存] {func.__name__}{args} → 缓存命中,直接返回!")
return cache[key]
return wrapper
@memoize
def fibonacci(n):
"""计算第 n 个斐波那契数(递归,非常耗时)"""
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2) # 递归调用
print(f"\n=== 缓存装饰器 ===")
print(f"fibonacci(30) = {fibonacci(30)}") # 首次计算,递归中大量重复调用被缓存
print(f"\nfibonacci(30) = {fibonacci(30)}") # 第二次:全部缓存命中,瞬间返回!
print(f"fibonacci(25) = {fibonacci(25)}") # 之前计算过,缓存命中
# 没有缓存的对比
def fib_no_cache(n):
"""无缓存的斐波那契——对比性能"""
if n <= 1:
return n
return fib_no_cache(n - 1) + fib_no_cache(n - 2)
print(f"\n无缓存 fib(15) = {fib_no_cache(15)}")
print("(没有缓存的 fib(30) 需要指数级时间,不演示了)")什么用:装饰器在 AI 项目中的应用非常广泛。@torch.no_grad() 是一个类装饰器/上下文管理器,用于推理时禁用梯度计算。@tf.function 将 Python 函数编译为 TensorFlow 计算图。@lru_cache(functools.lru_cache)是 Python 内置的缓存装饰器,常用于缓存数据预处理的结果。自定义的 @timer 用于性能分析,@retry 用于网络请求重试,@log_params 用于调试时打印函数参数。
二、带参数的装饰器——三层嵌套的"俄罗斯套娃"
是什么:有时候装饰器本身需要参数——比如 @log(level="INFO") 指定日志级别,@retry(times=3) 指定重试次数。带参数的装饰器需要三层嵌套:最外层接收装饰器参数,中间层接收被装饰函数,最内层是包装函数。@decorator(arg) 等价于 func = decorator(arg)(func)——先调用 decorator(arg) 返回一个装饰器,再用这个装饰器包装函数。
大白话 普通装饰器是两层套娃。带参数的装饰器是三层套娃——就像你去奶茶店点单:第一层是"我要一杯奶茶"(装饰器参数:糖度、冰量),第二层是"店员准备制作"(接收被装饰函数),第三层是"奶茶递到你手上"(包装后的函数被调用)。@decorator(args) 中的括号意味着"先执行 decorator(args),返回值才是真正的装饰器"。
为什么:带参数装饰器让你可以在应用装饰器时传递配置信息,而不是写死。比如 @retry(times=3, delay=1.0) 和 @retry(times=10, delay=0.1) 用同一个装饰器框架但不同的策略。这比"为每种配置写一个不同的装饰器"优雅得多。
怎么做:
import numpy as np
import time
from functools import wraps
# ====== 1. 带参数装饰器的结构——三层嵌套 ======
def repeat(times):
"""带参数的装饰器:让函数重复执行 times 次"""
def decorator(func): # 第二层:真正的装饰器
@wraps(func)
def wrapper(*args, **kwargs): # 第三层:包装函数
results = []
for i in range(times): # 使用外层参数 times
result = func(*args, **kwargs)
results.append(result)
return results
return wrapper
return decorator # 第一层返回装饰器
@repeat(times=3)
def roll_dice():
"""掷骰子——返回 1-6 的随机数"""
return np.random.randint(1, 7)
print("=== 带参数装饰器 ===")
print(f"掷骰子 3 次: {roll_dice()}")
# ====== 2. 理解执行流程 ======
print(f"\n解糖过程:")
print(f" repeat(times=3) 返回: {repeat(times=3)}")
print(f" 再用它包装 roll_dice: {repeat(times=3)(roll_dice)}")
# ====== 3. AI 场景:带重试机制的网络请求装饰器 ======
def retry(max_retries=3, delay=0.5, backoff=2):
"""
重试装饰器——自动重试失败的函数调用
max_retries: 最大重试次数
delay: 初始等待时间(秒)
backoff: 延迟倍增因子(指数退避)
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
current_delay = delay
for attempt in range(max_retries + 1):
try:
if attempt == max_retries:
print(f" [最后一次尝试] 调用 {func.__name__}...")
return func(*args, **kwargs)
except Exception as e:
if attempt < max_retries:
print(f" [重试 {attempt+1}/{max_retries}] "
f"{func.__name__} 失败: {e},"
f"等待 {current_delay:.1f}s...")
time.sleep(current_delay)
current_delay *= backoff # 指数退避
else:
print(f" [失败] {func.__name__} 重试 {max_retries} 次后仍失败")
raise
return None
return wrapper
return decorator
attempt_counter = 0
@retry(max_retries=3, delay=0.3, backoff=1.5)
def load_data_from_server(file_id):
"""模拟从服务器加载数据——有概率失败"""
global attempt_counter
attempt_counter += 1
np.random.seed(file_id * 10 + attempt_counter)
if np.random.random() < 0.6: # 60% 概率抛出异常
raise ConnectionError(f"无法连接到数据服务器(文件ID={file_id})")
print(f" 成功加载文件 {file_id}!")
return np.random.randn(100)
print(f"\n=== 重试装饰器 ===")
try:
data = load_data_from_server(file_id=42)
if data is not None:
print(f" 数据加载成功: 均值={np.mean(data):.4f}")
except ConnectionError:
print(" 最终加载失败,需要人工介入")
# ====== 4. AI 场景:装饰器栈(多个装饰器叠加) ======
def log_call(level="INFO", show_args=False):
"""日志装饰器——记录函数调用信息"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
call_id = np.random.randint(10000, 99999)
msg = f"[{level}] 调用 {func.__name__} (#{call_id})"
if show_args:
msg += f" args={args}, kwargs={kwargs}"
print(msg)
result = func(*args, **kwargs)
print(f"[{level}] {func.__name__} (#{call_id}) 返回")
return result
return wrapper
return decorator
@log_call(level="DEBUG", show_args=True)
@timer
def train_step(weights, data, lr=0.01):
"""模拟一个训练步骤"""
grad = np.mean(data) * weights
new_weights = weights - lr * grad
return new_weights
print(f"\n=== 装饰器栈 ===")
w = np.array([0.5])
d = np.array([1.0, 2.0, 3.0])
new_w = train_step(w, d, lr=0.1)
print(f" 权重更新: {w} → {new_w}")
# 装饰器从下往上包装,执行时像剥洋葱——从外到内进入,从内到外退出
print("\n装饰器从下往上包装,执行时像剥洋葱——从外到内进入,从内到外退出")什么用:带参数装饰器在 AI 工程化中非常重要。@retry 用于模型推理服务的自动重试;@cache(ttl=3600) 用于设置缓存过期时间;@profile(level="detailed") 用于不同粒度的性能分析。PyTorch 的 @torch.jit.script 也是带参数装饰器的一个变体。
三、类装饰器——用类的 __call__ 实现装饰逻辑
是什么:类装饰器用类实例作为装饰器。实现方式:__init__(self, func) 接收被装饰函数并保存;__call__(self, *args, **kwargs) 让实例可以被调用,在其中执行包装逻辑。类装饰器比函数装饰器更适合需要维护状态的场景——因为类实例可以在多次调用之间保存状态(如调用次数、累计耗时)。
大白话 函数装饰器像一个纸袋——你把函数放进去,它包一层还给你,用完就扔。类装饰器像一个工具箱——它不仅能包装函数,还能在工具箱里放计数器、计时器、日志本等工具。因为工具箱是持久的(类实例),每次用函数时都可以在工具箱里记录信息(更新实例属性)。
为什么:当你需要在装饰器中维护跨调用的状态时,函数装饰器需要用闭包 + nonlocal——状态多了代码就乱。类装饰器天然有 self 来管理状态——self.call_count、self.total_time、self.cache 等属性清晰直观。类装饰器还更容易添加方法——比如 reset() 重置状态,stats() 查看统计信息。
怎么做:
import numpy as np
import time
from functools import wraps
# ====== 1. 最简单的类装饰器 ======
class CountCalls:
"""类装饰器——统计函数被调用的次数"""
def __init__(self, func):
"""接收被装饰函数,保存引用"""
self.func = func
self.call_count = 0
wraps(func)(self) # 保留 __name__、__doc__
def __call__(self, *args, **kwargs):
"""让实例可以被调用——执行包装逻辑"""
self.call_count += 1
result = self.func(*args, **kwargs)
return result
def reset(self):
"""重置计数器——类装饰器的优势:可以添加方法"""
self.call_count = 0
@CountCalls
def add(a, b):
return a + b
@CountCalls
def multiply(a, b):
return a * b
print("=== 类装饰器 ===")
add(3, 4)
add(5, 6)
multiply(3, 4)
print(f"add 被调用: {add.call_count} 次") # 2
print(f"multiply 被调用: {multiply.call_count} 次") # 1
add.reset()
print(f"重置后 add.call_count: {add.call_count}") # 0
# ====== 2. AI 场景:模型调用统计装饰器 ======
class ModelProfiler:
"""
模型性能分析装饰器——统计调用次数、总耗时、平均耗时
可用于评估推理服务的性能瓶颈
"""
def __init__(self, func):
self.func = func
self.call_count = 0
self.total_time = 0.0
self.min_time = float("inf")
self.max_time = 0.0
self.call_times = []
wraps(func)(self)
def __call__(self, *args, **kwargs):
start = time.time()
result = self.func(*args, **kwargs)
elapsed = time.time() - start
self.call_count += 1
self.total_time += elapsed
self.min_time = min(self.min_time, elapsed)
self.max_time = max(self.max_time, elapsed)
if len(self.call_times) < 1000:
self.call_times.append(elapsed)
return result
def report(self):
"""生成性能报告"""
if self.call_count == 0:
return "尚未调用"
avg_time = self.total_time / self.call_count
p50 = np.percentile(self.call_times, 50) if self.call_times else 0
p95 = np.percentile(self.call_times, 95) if self.call_times else 0
return (
f" 函数: {self.func.__name__}\n"
f" 调用次数: {self.call_count}\n"
f" 总耗时: {self.total_time:.4f}s\n"
f" 平均耗时: {avg_time:.4f}s\n"
f" 最小/最大: {self.min_time:.4f}s / {self.max_time:.4f}s\n"
f" P50/P95: {p50:.4f}s / {p95:.4f}s"
)
@ModelProfiler
def inference(input_data):
"""模拟模型推理——耗时随机变化"""
time.sleep(np.random.uniform(0.01, 0.05))
weights = np.random.randn(10, 10)
output = np.dot(input_data.reshape(1, -1), weights)
return np.tanh(output)
print(f"\n=== 模型性能分析 ===")
for i in range(20):
data = np.random.randn(10)
result = inference(data)
print(inference.report())什么用:类装饰器在 AI 推理服务中非常实用。ModelProfiler 可以统计每个模型端点的吞吐量和延迟分布(P50/P95/P99),帮助发现性能瓶颈。类装饰器还能实现更复杂的功能——比如为多个模型端点维护共享的请求限流器(rate limiter),或维护一个全局的模型版本管理器。
四、functools.wraps 与装饰器最佳实践
是什么:functools.wraps 是一个装饰器工厂,用于在自定义装饰器的 wrapper 函数上保留原函数的元信息。没有 @wraps 时,被装饰函数的 __name__、__doc__、__module__ 等属性都会变成 wrapper 的——这在调试和文档生成时会造成混乱。@wraps(func) 把原函数的这些属性复制到 wrapper 上。
大白话 没有@wraps,就像你给手机套了个壳之后,手机壳外面印的不是手机品牌(iPhone),而是"手机壳"三个字。同事问你"这什么手机?"你只能说"是手机壳"——调试时也一样,func.__name__返回wrapper而不是真正的函数名。@wraps就是在手机壳外面正确印上 iPhone 的 logo。
为什么:装饰器会替换函数——decorated_func.__name__ 返回 wrapper 会严重干扰调试(尤其是用 IDE 跳转、查看文档时)。@wraps 解决了这个问题。此外,一些框架(如 Flask 的路由装饰器)依赖 __name__ 来注册端点,没有 @wraps 会导致路由注册失败。
怎么做:
import numpy as np
from functools import wraps
# ====== 1. 对比:有无 @wraps 的区别 ======
def bad_decorator(func):
"""没有 @wraps 的装饰器"""
def wrapper(*args, **kwargs):
"""这是 wrapper 的文档字符串"""
return func(*args, **kwargs)
return wrapper
def good_decorator(func):
"""有 @wraps 的装饰器"""
@wraps(func)
def wrapper(*args, **kwargs):
"""这是 wrapper 的文档字符串"""
return func(*args, **kwargs)
return wrapper
@bad_decorator
def bad_func():
"""这是 bad_func 的文档"""
return "bad"
@good_decorator
def good_func():
"""这是 good_func 的文档"""
return "good"
print("=== @wraps 的重要性 ===")
print(f"bad_func.__name__ = {bad_func.__name__}") # 'wrapper' ❌
print(f"good_func.__name__ = {good_func.__name__}") # 'good_func' ✅
# ====== 2. 装饰器最佳实践模板 ======
# 无参装饰器模板
def decorator_template(func):
"""装饰器最佳实践模板"""
@wraps(func) # ① 必须用 @wraps
def wrapper(*args, **kwargs): # ② 用 *args/**kwargs 转发所有参数
# ③ 前置逻辑(如日志、权限检查)
result = func(*args, **kwargs) # ④ 调用原函数
# ⑤ 后置逻辑(如格式化、缓存)
return result # ⑥ 返回原函数的结果
return wrapper
# ====== 3. AI 场景:输入验证装饰器 ======
def validate_shape(expected_shape):
"""
输入形状验证装饰器——确保 numpy 数组输入有正确的形状
expected_shape: 期望的形状,如 (None, 28, 28, 1)
None 表示该维度可以是任意大小
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
if args and isinstance(args[0], np.ndarray):
input_data = args[0]
actual_shape = input_data.shape
if len(actual_shape) != len(expected_shape):
raise ValueError(
f"{func.__name__} 期望 {len(expected_shape)} 维输入,"
f"实际得到 {len(actual_shape)} 维"
)
for i, (expected, actual) in enumerate(zip(expected_shape, actual_shape)):
if expected is not None and expected != actual:
raise ValueError(
f"{func.__name__} 第 {i} 维度不匹配:期望 {expected},实际 {actual}"
)
print(f" {func.__name__} 输入验证通过: shape={actual_shape}")
return func(*args, **kwargs)
return wrapper
return decorator
@validate_shape((None, 10)) # 期望形状: (任意行, 10 列)
def process_features(feature_matrix):
"""处理特征矩阵——每行一个样本,10 个特征"""
mean = np.mean(feature_matrix, axis=0)
std = np.std(feature_matrix, axis=0) + 1e-8
return (feature_matrix - mean) / std
print(f"\n=== 输入验证装饰器 ===")
correct_data = np.random.randn(5, 10) # (5, 10) ✅
result = process_features(correct_data)
print(f" 输出形状: {result.shape}")
wrong_data = np.random.randn(5, 20) # (5, 20) ❌
try:
process_features(wrong_data)
except ValueError as e:
print(f" 验证失败: {e}")
print("\n装饰器最佳实践总结:@wraps 保留元信息 | *args/**kwargs 转发参数 | 返回原函数结果")什么用:@wraps 在 AI 项目中不是可选的——是必须的。调试时你需要看 func.__name__ 而不是满屏的 wrapper。Sphinx/autodoc 生成 API 文档时依赖 __doc__ 和 __name__。PyTorch 内部大量使用 @wraps 来保证装饰后的函数在 JIT 编译时名称正确。输入验证装饰器在 ML 模型服务中非常常见——在调用模型前自动检查输入 tensor 的形状和类型。
概念关系图谱
| 概念 | 核心含义 | 与AI的关系 | 关联概念 |
|---|---|---|---|
| 装饰器 | 接受函数返回新函数的高阶函数 | @torch.no_grad()、性能分析 | 高阶函数、闭包 |
| @语法糖 | @deco ≡ func = deco(func) | 简化装饰器应用的语法 | 装饰器、函数替换 |
| @wraps | 保留被装饰函数的元信息 | 调试时看到正确函数名 | functools、装饰器 |
| 带参装饰器 | 三层嵌套的装饰器工厂 | @retry(times=3) 重试机制 | 闭包、工厂函数 |
| 类装饰器 | 用 __call__ 实现的装饰器 | 带状态的调用统计、性能分析 | __init__、__call__ |
| 装饰器栈 | 多个装饰器叠加使用 | 组合计时+日志+验证 | 洋葱模型、执行顺序 |
| 缓存装饰器 | 存储计算结果避免重复计算 | 数据预处理缓存、特征缓存 | Memoization、lru_cache |
| 验证装饰器 | 自动校验函数输入/输出 | 模型输入形状检查 | 类型检查、参数校验 |
重点答疑
Q1: 装饰器改了原函数,@wraps 之后真的完全一样吗?
不完全一样。@wraps 复制了 __name__、__doc__、__module__、__qualname__、__annotations__ 和 __wrapped__。但装饰后的函数和原函数在 id()、is 比较上仍然是不同的对象。如果你需要访问原始函数,可以通过 decorated_func.__wrapped__ 获取。
Q2: 类装饰器和函数装饰器什么时候各用哪个?
函数装饰器:逻辑简单、不需要跨调用维护状态。类装饰器:需要维护状态(调用计数、累计时间、统计信息),或者需要暴露额外方法(reset()、report()、stats())。简单规则——如果装饰器代码超过 15 行或有状态变量,考虑用类装饰器。
Q3: 装饰器的执行时机是什么时候?
装饰器在函数定义时执行,而不是在函数调用时。也就是说,@decorator 在 Python 加载模块、遇到 def 语句时就立即执行了——wrapper 函数在模块加载后就已经就位了。当你的代码真正调用被装饰函数时,执行的是已经包装好的 wrapper。
Q4: 如何实现一个既能当装饰器又能当上下文管理器的工具?
实现 __enter__/__exit__ 和 __call__ 方法在同一个类中。作为装饰器时,__call__ 返回包装函数;作为上下文管理器时,with 调用 __enter__/__exit__。contextlib.ContextDecorator 就是为此设计的——继承它并实现 __enter__/__exit__,自动获得装饰器能力。torch.no_grad() 就是这样实现的。
章节单词汇总
| 英文 | 音标 | 术语/释义 |
|---|---|---|
| decorator | /ˈdekəreɪtər/ | 装饰器;接受函数、返回新函数的高阶函数 |
| wrapper | /ˈræpər/ | 包装函数;装饰器内部用于包装原函数的函数 |
| syntactic sugar | /sɪnˈtæktɪk ˈʃʊɡər/ | 语法糖;@ 装饰器语法的别称 |
| functools | /fʌŋkˈtuːlz/ | Python 标准库模块;提供 wraps、lru_cache 等 |
| memoization | /ˌmeməraɪˈzeɪʃən/ | 记忆化;缓存计算结果以加速重复调用 |
| cross-cutting | /krɔːs ˈkʌtɪŋ/ | 横切的;跨多个模块的通用关注点(日志、安全等) |
| retry | /riːˈtraɪ/ | 重试;失败后自动重新执行的机制 |
| stack | /stæk/ | 栈;多个装饰器叠加时的洋葱模型结构 |
面试练习
Q1 [单选] @decorator 语法的等价写法是?
- A.
func = decorator() - B.
func = decorator - C.
func = decorator(func) - D.
decorator(func)
解答:C 正确。@decorator写在def func():前面时,Python 自动执行func = decorator(func)。@decorator(args)等价于func = decorator(args)(func)。
Q2 [单选] 以下关于 @wraps 的说法哪个是错误的?
- A.
@wraps保留原函数的__name__ - B.
@wraps保留原函数的__doc__ - C.
@wraps让被装饰函数和原函数是同一个对象(is比较返回 True) - D.
@wraps来自functools模块
解答:C 错误。@wraps 复制元信息但不改变对象身份——装饰后的函数和原函数仍然是不同对象。A、B、D 都正确。
Q3 [单选] 带参数装饰器需要几层嵌套?
- A. 2 层
- B. 3 层
- C. 4 层
- D. 1 层
解答:B 正确。三层:最外层接收装饰器参数 → 中间层接收被装饰函数 → 最内层是 wrapper。
Q4 [多选] 以下哪些是装饰器的常见应用?
- A. 日志记录
- B. 函数执行计时
- C. 输入参数验证
- D. 缓存计算结果(Memoization)
解答:A、B、C、D 全部正确。日志、计时、验证、缓存是装饰器最常见的四大应用场景。此外还有权限检查、重试机制、事务管理、性能分析等。
Q5 [单选] 类装饰器必须实现哪个方法?
- A.
__init__和__enter__ - B.
__init__和__get__ - C.
__init__和__call__ - D.
__init__和__new__
解答:C 正确。__init__接收被装饰函数,__call__让实例可以被调用(执行包装逻辑)。
Q6 [多选] 两个装饰器 @A 和 @B 同时使用时(@A @B def f(): pass),调用 f() 时 wrapper 的执行顺序是?
- A. A 的前置逻辑先于 B 的前置逻辑执行
- B. B 的后置逻辑先于 A 的后置逻辑执行
- D. A 的后置逻辑后于 B 的后置逻辑执行
解答:A、D 正确。装饰器栈执行像剥洋葱——A(外层)→B(内层)→原函数→B 后置→A 后置。编译时 f = A(B(f))。
Q7 [单选] functools.lru_cache 属于哪种装饰器?
- A. 无参装饰器
- C. 带参数装饰器(
@lru_cache(maxsize=128)) - D. 上下文管理器
解答:C 正确。@lru_cache(maxsize=128)需要传入maxsize参数来指定缓存大小。
Q8 [单选] 以下代码输出什么?def d(f): def w(): return f() + 1; return w; @d def g(): return 10; print(g())
- A. 10
- B. 11
- C. 报错
- D. None
解答:B 正确。@d把g替换为w,g()实际调用w()——调用原函数得到 10,然后+1返回 11。
Q9 [多选] 以下关于装饰器说法正确的是?
- A. 装饰器在函数定义时执行,而非函数调用时
- B. 装饰器可以叠加使用,离函数近的装饰器先包装
- C. 装饰器本质上是高阶函数
- D. 装饰器只能装饰函数,不能装饰类
解答:A、B、C 正确。D 错误——装饰器也可以用于装饰类。@dataclass、@functools.total_ordering都是装饰类的例子。
Q10 [单选] 如何获取被装饰函数的原始版本?
- A.
func.__original__ - B.
func.__func__ - C.
func.__wrapped__ - D. 无法获取
解答:C 正确。当装饰器使用了@wraps时,被装饰函数的原始版本可以通过decorated_func.__wrapped__属性访问。