Python 装饰器与元编程深度解析:从 @ 语法到类工厂

题眼: 装饰器是 Python 的 AOP------比 Spring AOP 更底层、更灵活。
本文适合有 Java 背景、正在学习 Python 的开发者。用 Spring AOP 作为认知参照系,从 @Transactional 的直觉出发,到装饰器的本质、进阶用法、类装饰器,最后抵达装饰器与元类的选型边界。核心洞察:Java 注解是元数据标记,Python 装饰器是运行时函数包装器。

第 1 章:第一印象 --- 当 @ 不再是注解

看两段等价代码,感受 @ 符号在两种生态中的完全不同含义。

给一个方法加上"执行时间超过 1 秒就打印警告"的逻辑:

java 复制代码
// Java: 用 Spring AOP 织入横切逻辑
@Aspect
@Component
public class TimingAspect {
    @Around("@annotation(Monitored)")
    public Object measure(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = pjp.proceed();
        long elapsed = System.currentTimeMillis() - start;
        if (elapsed > 1000) {
            System.out.println(pjp.getSignature() + " took " + elapsed + "ms");
        }
        return result;
    }
}

// 使用
@Monitored
public void processOrders() { ... }
python 复制代码
# Python: 写一个装饰器函数,加一个 @
import time
import functools

def monitor(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        if elapsed > 1:
            print(f"{func.__name__} took {elapsed:.2f}s")
        return result
    return wrapper

# 使用
@monitor
def process_orders():
    ...

两段代码达到同样的效果,但底层是完全不同的机制:

复制代码
Java Spring AOP:                          Python 装饰器:
┌──────────────────────────┐              ┌──────────────────────┐
│  @Aspect 类               │              │  def monitor(func):  │
│  + @Around advice        │              │      def wrapper():  │
│  + Spring 代理工厂        │              │          ...         │
│  + CGLIB/动态代理         │              │      return wrapper  │
│  → 生成代理对象           │              │  @monitor            │
│                          │              │  → process_orders    │
│  需要: Spring IoC + AOP  │              │     = monitor(       │
│        + AspectJ 注解     │              │       process_orders)│
└──────────────────────────┘              └──────────────────────┘
    框架级能力 + 多个组件协作                     语言级能力 + 一个函数

Java 翻译 : AOP(面向切面编程)是一个概念族------Spring AOP、AspectJ、Python 装饰器是不同的实现。本文用 Spring AOP 作为 Java 开发者最熟悉的参照系,不是说它们是一样的东西,而是帮你用已有的心智模型快速建立新的映射。最核心的认知翻译:Java 的注解是元数据(需要框架消费),Python 的装饰器是运行时行为修改(语言自己消费)。

这就是本文要展开的核心命题------从这个 @ 出发,看看 Python 用多简单的机制,实现了 Java 需要一整套框架才能做到的事。

第 2 章:一等函数 --- 装饰器的基石(引桥)

装饰器能存在,源于一个前提:Python 中函数是一等对象

python 复制代码
def greet(name):
    return f"Hello, {name}"

# 函数可以赋值给变量
g = greet
g("World")  # "Hello, World"

# 函数可以作为参数传递
def apply(func, arg):
    return func(arg)

apply(greet, "Python")  # "Hello, Python"

# 函数可以在内部定义并返回
def make_adder(n):
    def adder(x):
        return x + n   # 内部函数捕获外部变量 n ------ 这就是闭包
    return adder       # 返回函数本身

add_3 = make_adder(3)
add_3(5)  # 8

这三件事------赋值、传参、返回------构成了装饰器的全部前提。装饰器只是"接受函数、返回函数"的高阶函数。关于闭包的更深入讨论(变量捕获机制、延迟绑定陷阱),详见《Python 函数式特性深度解析》第 7 章。这里我们只需要一个结论:因为函数是一等对象,所以函数可以"被处理"------装饰器就是"处理函数的函数"。

Java 翻译 : Java 8 之前,函数不是一等公民------你需要用匿名内部类模拟回调。Java 8 引入 Function<T,R>Supplier<T>Consumer<T> 和 lambda 表达式,函数才变得可传递。但 Java 的 lambda 本质上是函数式接口的语法糖------编译后是一个匿名类的实例,不是独立的一等对象。Python 的 def 创建的是一个真正的 function 对象,和 intstr 一样,可以随时赋给变量、传参、返回。

第 3 章:装饰器本质 --- @ 只是一个赋值

3.1 @ 是语法糖

PEP 318(2003 年)引入装饰器语法。它的核心设计决策可以用一行代码概括:

python 复制代码
@decorator
def func():
    pass

# 等价于
def func():
    pass
func = decorator(func)

@decorator 就是 func = decorator(func) 的语法糖。装饰器是一个接受函数、返回函数的函数。没有更多魔法。

3.2 最简单的装饰器

python 复制代码
def uppercase(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

@uppercase
def greet(name):
    return f"hello, {name}"

greet("world")  # "HELLO, WORLD"

执行过程:

复制代码
greet("world")
  │
  └─ 实际调用的是 wrapper("world")
       │
       ├─ 1. 调用原函数 func("world") → "hello, world"
       ├─ 2. 转换结果为 .upper() → "HELLO, WORLD"
       └─ 3. 返回转换后的结果

3.3 PEP 318 的设计取舍

PEP 318 的讨论过程透露了 Python 社区的一个典型设计偏好。当时有几种候选语法:

python 复制代码
# 候选 1: 放在 def 之前(最终采用)
@decorator
def func(): pass

# 候选 2: 放在 def 之后
def func() @decorator: pass

# 候选 3: 放在参数列表中
def func(@decorator): pass

最终选定了前置 @ 语法,核心理由是:装饰器的作用是"转换函数定义",放在 def 之前强调了"对定义的修饰"这一语义 。PEP 318 原文强调:"The @ character was chosen because it's not used in Python syntax, and because it vaguely resembles the pie-decorator syntax."

Java 翻译 : Java 的注解(annotation)也使用 @ 符号------比如 @Override@Transactional。但这是两种完全不同的机制。Java 注解是元数据------它们被存储在 .class 文件的注解表中,等待框架或编译器通过反射消费。Python 装饰器是运行时行为------@ 后面跟着一个函数调用表达式,它在模块加载时立即执行。一个简单的测试:Java 的 @Deprecated 不会改变方法的执行逻辑;Python 的装饰器一定会改变函数的运行时行为。

3.4 装饰器的执行时机

注意:装饰器在函数定义时执行,不在函数调用时执行:

python 复制代码
def register(func):
    print(f"Registering {func.__name__}")
    return func  # 不改函数,只注册

@register  # 这行立即打印 "Registering foo"
def foo():
    pass

foo()      # 什么都不打印 ------ 装饰器已经在定义时执行过了
foo()      # 仍然什么都不打印

这种"定义时执行+不改函数"的模式,是 Python Web 框架中 @app.route("/") 等注册型装饰器的基础。

装饰器从"做什么"的角度可分为两类:

类型 模式 代表 Java 类比
改行为型 return wrapper,替换原函数 @timer, @retry, @cache AOP around advice
注册型 return func,不改原函数 @app.route, @register, @pytest.fixture 注解 + 包扫描

改行为型的标识:装饰器内定义了 wrapper 函数,最终 return wrapper------原函数被替换了。

注册型的标识:装饰器在函数定义时做了副作用(如加到注册表),但 return func 原封不动------函数还是那个函数,只是多了一个"已记录"的标记。

python 复制代码
# 注册型装饰器的最小实现
handlers = {}

def route(path):
    def decorator(func):
        handlers[path] = func  # 副作用:注册
        return func            # 不改原函数
    return decorator

@route("/users")
def list_users():
    return ["Alice", "Bob"]

print(handlers)  # {'/users': <function list_users>}
print(list_users())  # ['Alice', 'Bob']  ← 函数行为毫无变化

这种模式是 Python Web 框架(Flask、FastAPI)、测试框架(pytest 的 @fixture)、插件系统的基石。Java 中类似的需求通常靠"注解标记 + 包扫描 + 反射"实现------Python 用 10 行代码就完成了等价的注册机制。

第 4 章:带参装饰器 --- 再加一层函数

4.1 问题:装饰器怎么接受参数?

如果需要"执行超过 N 秒才警告",N 作为参数传入:

python 复制代码
@monitor(threshold=2.0)  # 超过 2 秒才警告
def slow_function():
    ...

这等价于:

python 复制代码
slow_function = monitor(threshold=2.0)(slow_function)
#               └──────┬──────┘ └──────┬──────┘
#                 第 1 步:工厂调用       第 2 步:装饰器调用

所以 monitor(threshold=2.0) 的返回值必须是一个装饰器。这就是"装饰器工厂"------再加一层函数。

4.2 装饰器工厂的三层结构

复制代码
┌─────────────────────────────────────────────────────────┐
│  第 1 层:工厂函数(接收参数)                              │
│  def monitor(threshold=1.0):          ← 接收配置参数      │
│      ┌─────────────────────────────────────────────────┐ │
│      │ 第 2 层:装饰器函数(接收被装饰函数)                 │ │
│      │ def decorator(func):          ← 接收原函数        │ │
│      │     ┌──────────────────────────────────────────┐ │ │
│      │     │ 第 3 层:包装器函数(接收调用参数)            │ │ │
│      │     │ @functools.wraps(func)                   │ │ │
│      │     │ def wrapper(*args, **kwargs):            │ │ │
│      │     │     ...  # 实际增强逻辑                    │ │ │
│      │     │     return func(*args, **kwargs)         │ │ │
│      │     │ return wrapper                           │ │ │
│      │     └──────────────────────────────────────────┘ │ │
│      │ return decorator                                │ │
│      └─────────────────────────────────────────────────┘ │
│  return decorator                                       │
└─────────────────────────────────────────────────────────┘

@monitor(threshold=2.0)
def slow_func():
    ...

完整实现:

python 复制代码
import time
import functools

def monitor(threshold=1.0):
    """装饰器工厂:threshold 秒以上的调用打印警告"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            start = time.time()
            result = func(*args, **kwargs)
            elapsed = time.time() - start
            if elapsed > threshold:
                print(f"[WARN] {func.__name__} took {elapsed:.2f}s (threshold={threshold}s)")
            return result
        return wrapper
    return decorator

Java 翻译 : 这类似于 Spring 中带属性的注解 @Cacheable(value="users", key="#id")。区别在于:Java 的注解属性在编译期/运行期由框架通过反射读取;Python 的装饰器参数在模块加载时就被函数调用消费,生成了一个新的闭包包装器。一层是"框架读你的配置",一层是"你自己执行了配置逻辑"。

4.3 一个常见的困惑:不带括号也能用?

python 复制代码
@monitor           # 不带括号:monitor 本身是装饰器
def func_a(): ...

@monitor(threshold=2.0)  # 带括号:monitor(...) 返回装饰器
def func_b(): ...

区别在于 monitor 的定义方式。如果想让同一个名字同时支持 @monitor@monitor() 两种用法,需要加一点判断:

python 复制代码
def monitor(func=None, *, threshold=1.0):
    """同时支持 @monitor 和 @monitor(threshold=2.0)"""
    if func is not None:
        # @monitor 用法:直接作为装饰器
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            start = time.time()
            result = func(*args, **kwargs)
            if time.time() - start > threshold:
                print(f"[WARN] {func.__name__} took too long")
            return result
        return wrapper
    else:
        # @monitor(threshold=2.0) 用法:返回装饰器
        return lambda f: monitor(f, threshold=threshold)

第 5 章:@wraps 与装饰器堆叠

5.1 为什么需要 @wraps?

不加 @functools.wraps,装饰器会"偷走"原函数的身份:

python 复制代码
def bad_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@bad_decorator
def hello():
    """Say hello."""
    pass

print(hello.__name__)  # "wrapper"  ← 不是 "hello"!
print(hello.__doc__)   # None       ← 文档字符串丢了!

@functools.wraps(func) 的作用:把 func__name____doc____module____qualname____dict__ 等元信息复制到 wrapper 上。它还设置 wrapper.__wrapped__ = func,让你可以访问被包装的原函数。

python 复制代码
def good_decorator(func):
    @functools.wraps(func)   # ← 加上这行
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@good_decorator
def hello():
    """Say hello."""
    pass

print(hello.__name__)    # "hello"       ← 正确
print(hello.__doc__)     # "Say hello."  ← 正确
print(hello.__wrapped__) # <function hello>  ← 可以访问原函数

5.2 装饰器堆叠:从下往上执行

python 复制代码
@decorator_a   # 第 3 步执行
@decorator_b   # 第 2 步执行
@decorator_c   # 第 1 步执行
def func():
    pass

# 等价于
# func = decorator_a(decorator_b(decorator_c(func)))

执行顺序是自下而上 的。运行时调用顺序是自上而下的:

python 复制代码
def add_tag(tag):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            return f"<{tag}>{func(*args, **kwargs)}</{tag}>"
        return wrapper
    return decorator

@add_tag("div")    # 第 3 层:外
@add_tag("span")   # 第 2 层:中
@add_tag("b")      # 第 1 层:内
def text():
    return "Hello"

print(text())  # <div><span><b>Hello</b></span></div>
#               └── 外层 ───────────────┘
#                    └── 中层 ──────┘
#                         └─内层─┘
复制代码
装饰器堆叠的时序图:

定义阶段(自下而上):
  add_tag("b")(text)     → wrapper_b
  add_tag("span")(wrapper_b) → wrapper_span
  add_tag("div")(wrapper_span) → wrapper_div

调用阶段(自上而下):
  wrapper_div()
    → wrapper_span()
      → wrapper_b()
        → 原 text() → "Hello"
      → "<b>Hello</b>"
    → "<span><b>Hello</b></span>"
  → "<div><span><b>Hello</b></span></div>"

Java 翻译 : Spring AOP 的 advice 也有执行顺序------@Order 注解控制多个切面的优先级。区别在于:Spring AOP 的 advice 链由代理对象管理(运行时动态),Python 装饰器的堆叠在函数定义时就固定了(模块加载时)。一个运行时可变,一个定义时不可变------各有利弊。

5.3 经典组合模式

python 复制代码
@retry(max_attempts=3, backoff=2.0)   # 最外层:重试策略
@monitor(threshold=1.0)               # 中层:监控
@functools.lru_cache(maxsize=128)      # 内层:缓存
def fetch_data(url):
    return requests.get(url).json()

推荐堆叠顺序:缓存 → 监控/日志 → 重试/容错。缓存在最内层(避免重复计算),容错在最外层(最后一层保护)。

5.4 装饰器的性能开销

装饰器本质是多一次函数调用。每次调用被装饰的函数时,实际调用链是 wrapper → 原函数。单层装饰器增加约 1.5~2 倍函数调用开销(约 50-100ns,取决于 Python 版本),三层堆叠约 3~5 倍。

python 复制代码
import timeit

def raw():
    return 1

@timer        # 单层
def decorated():
    return 1

# 微基准:100 万次调用(CPython 3.12)
# raw:        ~0.08s
# decorated:  ~0.12s  (约 1.5x)
# 三层堆叠:   ~0.18s  (约 2.3x)

对于绝大多数业务代码(Web 请求、数据处理、文件 I/O),这个开销完全可以忽略------一次数据库查询的耗时是它的 10 万倍。不要因为"可能有性能开销"就不写装饰器。 只在极高频的热路径(如数值计算内核)中才需要考虑手动内联。

5.5 类型提示:让装饰器不破坏签名

装饰器有一个隐性代价:wrapper(*args, **kwargs) 会丢失原函数的参数类型信息。对使用 mypy/pyright 的项目,这意味着装饰后的函数失去了类型检查。

Python 3.10 引入的 ParamSpec 解决了这个问题:

python 复制代码
from typing import Callable, TypeVar, ParamSpec
import functools
import time

P = ParamSpec("P")
R = TypeVar("R")

def monitor(func: Callable[P, R]) -> Callable[P, R]:
    @functools.wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        start = time.time()
        result = func(*args, **kwargs)
        print(f"{func.__name__} took {time.time() - start:.2f}s")
        return result
    return wrapper

@monitor
def greet(name: str, times: int = 1) -> str:
    return f"Hello, {name}! " * times

# mypy 现在知道 greet 的参数类型是 (str, int) → str
# 而不是 (*args: Any, **kwargs: Any) → Any

ParamSpecP.argsP.kwargs 捕获了原函数的完整参数签名,TypeVar 保留了返回类型。Python 3.10 之前,需要用 Callable[..., R] 或手动写重载------前者丢失所有类型信息,后者维护成本极高。

类型提示在装饰器中的深入应用,参见《Python 类型系统深度解析》。本文聚焦于"让装饰器的类型签名不丢失"这一最小可用场景。

第 6 章:类装饰器 --- 装饰整个类

6.1 装饰器也可以装饰类

PEP 3129(2007 年)将装饰器语法扩展到类:

python 复制代码
@decorator
class MyClass:
    ...

# 等价于
class MyClass:
    ...
MyClass = decorator(MyClass)

6.2 @dataclass:最著名的类装饰器

python 复制代码
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

# 这自动生成了 __init__、__repr__、__eq__
p = Point(1.5, 2.5)
print(p)  # Point(x=1.5, y=2.5)

@dataclass 做了什么?它在类定义完成后,扫描类的类型标注,自动生成方法并注入:

复制代码
@dataclass 的工作流程:
  1. Python 执行 class body,创建属性字典 namespace
  2. type() 创建 Point 类对象
  3. @dataclass 装饰器函数被调用,接收 Point 类对象
  4. dataclass 检查 __annotations__,生成 __init__/__repr__/__eq__
  5. 将这些方法注入 Point 类对象
  6. 返回修改后的 Point 类对象

Java 翻译 : Java 的 @Entity@Component@Service 是注解标记,ORM 框架(Hibernate)或 IoC 容器(Spring)扫描这些注解并处理。Python 的类装饰器更直接------它修改了类本身 。Java 的 @Entity 不改变类的方法表;Python 的 @dataclass 会给类注入新方法。Lombok 是最接近的 Java 类比------它在编译期 生成代码;Python 类装饰器在运行时修改类。

6.3 写一个类装饰器:自动添加 repr

python 复制代码
import functools

def add_repr(cls):
    """为类添加一个自动的 __repr__ 方法"""
    def __repr__(self):
        attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
        return f"{cls.__name__}({attrs})"
    cls.__repr__ = __repr__
    return cls

@add_repr
class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

u = User("Alice", 30)
print(u)  # User(name='Alice', age=30)

6.4 函数装饰器 vs 类装饰器

函数装饰器 类装饰器
接收 函数对象 类对象
返回 通常是 wrapper 函数 通常是修改后的类
典型操作 包裹调用,注入 before/after 注入方法/属性,修改类行为
代表 @timer, @retry, @cache @dataclass, @total_ordering
Java 类比 AOP around advice 编译期注解处理(Lombok)

第 7 章:内置装饰器巡礼 --- Python 的内置 AOP 点

Python 内置了几个装饰器,它们不是语法糖,而是语言层面的 AOP 切点------每个都拦截了一种特定的访问模式。

7.1 @property:拦截属性访问

python 复制代码
class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value <= 0:
            raise ValueError("Radius must be positive")
        self._radius = value

c = Circle(5)
print(c.radius)   # 5       ------ 看起来是属性访问,实际是方法调用
c.radius = 10     #         ------ 看起来是属性赋值,实际是 setter 调用

@property 的底层是 descriptor 协议(详见《Python OOP 体系深度解析》第 2-3 章),但从使用角度看,它是一个属性访问拦截器 ------把 obj.attr 的读取和赋值转化为方法调用。

Java 翻译 : Java 社区有 getter/setter 惯例(getRadius()/setRadius()),Python 的 @property 用属性访问语法替代了这种惯例。区别在于:Java 的 getter/setter 是命名约定,编译器不强制执行;Python 的 @property 是语言机制,.radius 的读取一定会触发 getter。

7.2 @staticmethod 与 @classmethod:拦截方法绑定

python 复制代码
class MyClass:
    @staticmethod
    def static_method():
        return "no self/cls needed"

    @classmethod
    def class_method(cls):
        return f"cls is {cls.__name__}"

    def instance_method(self):
        return f"self is {self}"

@staticmethod 阻止 Python 自动传入 self@classmethod 让 Python 传入 cls(类本身)而非 self(实例)。用法详见《Python OOP 基础认知翻译》。从 AOP 视角看:这两个装饰器修改了方法描述符的绑定行为------它们通过 descriptor 协议拦截了方法在类上的访问方式。

7.3 @abstractmethod:拦截实例化

python 复制代码
from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        return "Woof!"

# Animal()  ← TypeError: Can't instantiate abstract class
Dog()      # OK

@abstractmethod 在类创建时标记方法为抽象,阻止直接实例化。它本质上是一个类创建期拦截器------与 metaclass 配合,但不要求你手动写 metaclass。

7.4 @cached_property:拦截首次访问

python 复制代码
from functools import cached_property

class DataLoader:
    @cached_property
    def data(self):
        print("Loading...")
        return [1, 2, 3, 4, 5]

dl = DataLoader()
print(dl.data)  # "Loading..." → [1, 2, 3, 4, 5]
print(dl.data)  # [1, 2, 3, 4, 5]  ← 不打印 "Loading...",已缓存

首次访问时执行方法并将结果存入实例 __dict__,后续访问直接返回缓存值。本质是"惰性初始化 + 缓存",但用 @property 的语义表达。

更多内置装饰器用法:@lru_cache(详见《Python 函数式特性深度解析》第 8 章)、@singledispatch@contextmanager 等。每个都是 Python 在语言层面提供的 AOP 切点------不用引入框架,一行 @ 获得横切能力。

第 8 章:装饰器 vs 元类 --- 何时升级

8.1 三种拦截层级

复制代码
┌─────────────────────────────────────────────────────────┐
│                    Python 的拦截层级                      │
│                                                         │
│  L1: 装饰器                                             │
│      拦截函数调用                                       │
│      工具: @decorator                                  │
│      示例: @timer, @retry, @log, @cache                │
│      → 对 Java 开发者: 类似 AOP around advice            │
│                                                         │
│  L2: 类装饰器                                           │
│      拦截类创建后、使用前                                 │
│      工具: @class_decorator                             │
│      示例: @dataclass, @total_ordering                  │
│      → 对 Java 开发者: 类似 Lombok 编译期处理             │
│                                                         │
│  L3: 元类 (metaclass)                                   │
│      拦截类的创建过程本身                                 │
│      工具: class MyClass(metaclass=MyMeta):             │
│      示例: ORM Model 字段收集、单例、抽象基类              │
│      → 对 Java 开发者: 类似 ByteBuddy/CGLIB 字节码增强    │
│      ▸ 实现细节详见《Python OOP 体系深度解析》第 5 章       │
└─────────────────────────────────────────────────────────┘

8.2 选型决策矩阵

场景 使用 原因
给函数加计时/日志/重试 装饰器 简单,一个函数搞定
给多个函数加相同的横切逻辑 装饰器 可复用,可堆叠
需要在多个地方注册函数/类 装饰器 @register 模式
给类自动生成方法(如 __init__ 类装饰器 @dataclass 就是典范
子类定义时自动触发注册 __init_subclass__ 比 metaclass 轻 10 倍
需要在类创建时修改属性字典 metaclass 类装饰器在类已创建后执行,无法修改 namespace
需要控制实例创建(如单例) metaclass __call__ 拦截 cls() 的最干净方式

8.3 从装饰器到元类的升级信号

什么时候该从装饰器升级到 metaclass?

复制代码
装饰器 → 类装饰器:
  你发现自己写了 5 个类似的函数装饰应用在类的方法上
  → 合并成一个类装饰器

类装饰器 → __init_subclass__:
  你的类装饰器只是做"在字典里注册这个类"
  → 用 __init_subclass__ 更干净

__init_subclass__ → metaclass:
  你需要拦截类的属性定义(不是类创建后修改,而是影响创建过程本身)
  → 升级到 metaclass

metaclass 的完整教程(type() 工厂、__new__/__init__/__prepare__、策略模式选择、冲突处理)详见《Python OOP 体系深度解析》第 5 章。那里的第 5.6 节有一个完整的选型决策框架流程图。

第 9 章:常见陷阱

陷阱 1: 装饰器忘记返回 wrapper

python 复制代码
def broken_decorator(func):
    def wrapper(*args, **kwargs):
        print("before")
        return func(*args, **kwargs)
    # ← 忘记 return wrapper!

@broken_decorator
def greet():
    return "hello"

print(greet)   # None  ← 装饰器返回了 None!

陷阱 2: 忘记 @wraps

见第 5 章。后果:func.__name__ 变成 "wrapper"、docstring 丢失、调试困难。

陷阱 3: 带参装饰器少写一层

python 复制代码
# 错误:outer 被当成了装饰器,但用户想传参数
def outer(func, threshold=1.0):
    ...

@outer  # 正确 ------ outer 是装饰器
@outer(threshold=2.0)  # 错误!outer() 返回 None(没有 return 装饰器)

正确写法:需要三层(工厂 → 装饰器 → 包装器),第 4 章已详解。

陷阱 4: 装饰器堆叠顺序混淆

python 复制代码
@log         # 先包裹
@cache       # 后包裹
def func():
    pass

定义顺序:@log 在最外层。所以调用时 log 先执行,cache 后执行,原函数最后执行。记法:def 越近,越接近原函数。

陷阱 5: 类方法装饰器丢失 self

python 复制代码
def log_return(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print(f"Returned: {result}")
        return result
    return wrapper

class MyClass:
    @log_return
    def method(self, x):
        return x * 2

# 正常工作 ------ *args 中第一个参数就是 self
MyClass().method(5)  # "Returned: 10"

但如果装饰器假设第一个参数是特定类型:

python 复制代码
def validate_int(func):
    @functools.wraps(func)
    def wrapper(value):  # ← 只接收一个参数
        if not isinstance(value, int):
            raise TypeError
        return func(value)
    return wrapper

class Calc:
    @validate_int
    def double(self, value):
        return value * 2

# Calc().double(5)  ← TypeError: wrapper() takes 1 positional argument
# 因为 self 被当成了 value!

对策 : 装饰器总是用 *args, **kwargs 透传参数,不做参数数量的假设。

陷阱 6: 装饰器工厂中的可变默认参数

python 复制代码
def register(registry=[]):    # ← 危险:可变默认参数
    def decorator(func):
        registry.append(func)
        return func
    return decorator

@register()
def foo(): pass

@register()
def bar(): pass

# registry 在所有 @register() 之间共享!

虽然这对于注册模式来说可能是预期行为(所有被装饰函数共享同一个注册表),但如果意图是每次 @register() 创建一个新列表,那就是 bug。Python 的可变默认参数陷阱(详见《Python 函数式特性深度解析》第 9 章)在装饰器工厂中同样存在。

陷阱 7: 装饰器对 help()inspect 的影响

即使加了 @wrapsinspect.signature() 可能仍然不准确。对于需要精确类型提示的场景(如 FastAPI 的依赖注入),考虑用 @wraps + __wrapped__ 配合类型系统,或使用 ParamSpec(Python 3.10+)保留参数签名。

第 10 章:收束 --- 选型框架与关键收获

10.1 选型决策框架

复制代码
你需要横切逻辑作用于什么?

  ┌─ 函数 ──▶ @decorator
  │          例: @timer, @retry, @cache, @log
  │
  ├─ 类(创建后修改)──▶ @class_decorator
  │                     例: @dataclass, @total_ordering
  │
  ├─ 类(定义时注册)──▶ __init_subclass__
  │                     例: 插件自动注册
  │
  └─ 类(创建过程控制)──▶ metaclass
                         例: ORM 字段收集、抽象基类
                         ▸ 详见《Python OOP 体系深度解析》第 5 章

10.2 装饰器 vs Spring AOP:本质差异对比

维度 Spring AOP Python 装饰器
实现机制 动态代理(JDK Proxy / CGLIB) 高阶函数包装
织入时机 运行时(Bean 初始化时创建代理) 定义时(模块加载时执行)
切点粒度 方法执行 整个函数调用
配置方式 注解 + 切面类 + Spring 配置 一个函数 + @
依赖 Spring IoC 容器 零依赖
环绕通知 @Around + ProceedingJoinPoint 装饰器函数内手动 return func(...)
参数传递 JoinPoint.getArgs() 直接的 *args, **kwargs
调试 代理链路复杂,需要理解代理对象 直接看装饰器函数,栈追踪简单

10.3 生产排查工具

python 复制代码
import functools
import inspect

# 1. 查看被装饰函数的原始函数
@functools.lru_cache(maxsize=128)
def fibonacci(n):
    return n if n < 2 else fibonacci(n-1) + fibonacci(n-2)

print(fibonacci.__wrapped__)  # <function fibonacci>

# 2. 检查一个对象是不是被装饰过的
def is_decorated(func):
    return hasattr(func, "__wrapped__")

# 3. 获取装饰器栈(从外到内)
def get_decorator_chain(func):
    chain = []
    while hasattr(func, "__wrapped__"):
        chain.append(func)
        func = func.__wrapped__
    chain.append(func)  # 最内层的原始函数
    return chain

# 4. 查看函数签名(可能被装饰器改变)
sig = inspect.signature(fibonacci)
print(sig)  # (n)

# 5. 调试装饰器的执行时机
import dis
# dis.dis(some_decorated_function)  # 查看字节码,确认装饰器的影响

10.4 关键收获

  1. 装饰器就是高阶函数 --- @decorator 等价于 func = decorator(func)。没有更多魔法,理解这一点就掌握了装饰器的全部。

  2. AOP 不需要框架 --- Python 用语言级别的装饰器实现了 Java 需要 Spring + AspectJ 才能做到的事。不是"更好",是"更原生"------没有代理对象,没有容器依赖,直接函数包裹。

  3. 三层拦截体系 --- 装饰器管函数调用 → 类装饰器管类属性 → metaclass 管类创建。选型取决于你需要拦截的层级。大多数场景装饰器就够,__init_subclass__ 次之,metaclass 是最后的选择。

  4. 带参装饰器 = 再加一层 --- 这不是 hack,是"高阶函数 + 闭包"模式的自然延伸。三层结构(工厂→装饰器→包装器)理解了就不会写错。

  5. 堆叠顺序是 bottom-up --- 离 def 越近的装饰器越先包裹原函数。调用的执行顺序恰好相反:最外层的装饰器最先执行。

  6. @wraps 是必需品 --- 不带 @wraps 的装饰器会窃取函数的身份信息(__name____doc__),影响调试、文档生成和任何依赖函数元信息的工具。

  7. 第二层收官 --- 本文是 Python 专家系列第二层"设计范式"的最后一篇。写完本文,从运行时机制(内存/并发/导入/异常)到设计范式(类型/OOP/函数式/装饰器),Python 语言层面的"认知翻译"体系完整闭合。之后进入第三层:工程实践。

系列完成: 基础语法翻译是引桥,本文是第二层收官。第一层讲"代码怎么活着"(内存、并发、导入、异常),第二层讲"代码怎么写"(类型、OOP、函数式、装饰器)。此后进入第三层"代码怎么交付"------从测试到打包,从性能剖析到 IO 与序列化,转入工程实践的新篇章。