题眼: 装饰器是 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对象,和int、str一样,可以随时赋给变量、传参、返回。
第 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
ParamSpec 让 P.args 和 P.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 的影响
即使加了 @wraps,inspect.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 关键收获
-
装饰器就是高阶函数 ---
@decorator等价于func = decorator(func)。没有更多魔法,理解这一点就掌握了装饰器的全部。 -
AOP 不需要框架 --- Python 用语言级别的装饰器实现了 Java 需要 Spring + AspectJ 才能做到的事。不是"更好",是"更原生"------没有代理对象,没有容器依赖,直接函数包裹。
-
三层拦截体系 --- 装饰器管函数调用 → 类装饰器管类属性 → metaclass 管类创建。选型取决于你需要拦截的层级。大多数场景装饰器就够,
__init_subclass__次之,metaclass 是最后的选择。 -
带参装饰器 = 再加一层 --- 这不是 hack,是"高阶函数 + 闭包"模式的自然延伸。三层结构(工厂→装饰器→包装器)理解了就不会写错。
-
堆叠顺序是 bottom-up --- 离
def越近的装饰器越先包裹原函数。调用的执行顺序恰好相反:最外层的装饰器最先执行。 -
@wraps 是必需品 --- 不带
@wraps的装饰器会窃取函数的身份信息(__name__、__doc__),影响调试、文档生成和任何依赖函数元信息的工具。 -
第二层收官 --- 本文是 Python 专家系列第二层"设计范式"的最后一篇。写完本文,从运行时机制(内存/并发/导入/异常)到设计范式(类型/OOP/函数式/装饰器),Python 语言层面的"认知翻译"体系完整闭合。之后进入第三层:工程实践。
系列完成: 基础语法翻译是引桥,本文是第二层收官。第一层讲"代码怎么活着"(内存、并发、导入、异常),第二层讲"代码怎么写"(类型、OOP、函数式、装饰器)。此后进入第三层"代码怎么交付"------从测试到打包,从性能剖析到 IO 与序列化,转入工程实践的新篇章。