装饰器:函数装饰器、类装饰器

一句话概述

装饰器(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_cachefunctools.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_countself.total_timeself.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()、性能分析高阶函数、闭包
@语法糖@decofunc = 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 标准库模块;提供 wrapslru_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 正确。@dg 替换为 wg() 实际调用 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__ 属性访问。