10.2 装饰器
前置:3.4(闭包)、3.3(
*args/**kwargs) · 约 30 分钟 · 官方对应:术语:装饰器、函数定义(参考)、functools
目标
- 理解
@deco只是f = deco(f)的语法糖。 - 读懂通用装饰器模板:闭包 +
*args, **kwargs+functools.wraps。 - 读懂带参数的装饰器(三层嵌套)与多个装饰器的叠加顺序。
- 认识常见内置/标准库装饰器:
@property、@staticmethod、@classmethod、@dataclass、@functools.cache、@contextmanager。
核心概念
本质:函数替换
Python · 可运行
python
def shout(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
@shout # 等价于:greet = shout(greet)
def greet(name):
return f"hello, {name}"
print(greet("python"))
def plain(name):
return f"hi, {name}"
plain = shout(plain) # 手动写法,效果完全相同
print(plain("python"))装饰器是一个接收函数、返回(通常是新的)函数的可调用对象。被装饰后,名字 greet 绑定的是 wrapper。
标准模板
Python · 可运行
python
import functools
import time
def timed(func):
@functools.wraps(func) # 复制原函数的 __name__、__doc__ 等元信息
def wrapper(*args, **kwargs): # 接收任意参数,原样转发
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
cost = (time.perf_counter() - start) * 1000
print(f"{func.__name__} 耗时 {cost:.2f} ms")
return wrapper
@timed
def get_weather(city: str) -> str:
"""查询城市天气。"""
return f"{city}:晴,22°C"
print(get_weather("上海"))
print(get_weather.__name__, get_weather.__doc__) # 有 wraps:保留原名与文档带参数的装饰器
@retry(times=3) 先调用 retry(times=3) 得到真正的装饰器,再用它装饰函数——因此是三层嵌套:
Python · 可运行
python
import functools
def retry(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except ConnectionError as e:
print(f"第 {attempt} 次失败:{e}")
raise RuntimeError(f"重试 {times} 次仍失败")
return wrapper
return decorator
calls = {"n": 0}
@retry(times=3) # 等价于:flaky = retry(times=3)(flaky)
def flaky():
calls["n"] += 1
if calls["n"] < 3:
raise ConnectionError("网络抖动")
return "成功"
print(flaky())叠加顺序
离函数最近的装饰器最先应用:
Python · 可运行
python
def tag(name):
def deco(func):
def wrapper():
return f"<{name}>{func()}</{name}>"
return wrapper
return deco
@tag("b")
@tag("i") # 先应用:hello = tag("b")(tag("i")(hello))
def hello():
return "hi"
print(hello()) # <b><i>hi</i></b>常见装饰器
Python · 可运行
python
import functools
@functools.cache # 记忆化:相同参数只计算一次(3.9 起)
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(80), fib.cache_info().hits > 0)| 装饰器 | 作用 | 课程 |
|---|---|---|
@property / @classmethod / @staticmethod | 方法类型 | 7.2 |
@dataclass | 生成数据类方法(装饰类) | 7.3 |
@contextmanager | 生成器 → 上下文管理器 | 6.3 |
@functools.cache / lru_cache | 结果缓存 | 本课 |
@functools.cached_property | 首次访问时计算并缓存的属性 | 10.4 |
@overload / @override | 仅供类型检查 | 9.2 |
框架装饰器:@app.get("/")、@pytest.fixture、@tool | 注册路由/夹具/工具 | — |
易错点
- 漏写
functools.wraps:被装饰函数的__name__变成wrapper,日志、调试、文档工具都会显示错误名字。 - wrapper 忘记
return:被装饰函数总返回None。 - 带参数装饰器漏了括号:
@retry而不是@retry(times=3),此时retry收到的是函数本身,后续调用报TypeError。 - 装饰器在定义时执行:
@register中的注册逻辑在模块导入时就运行了,而不是函数被调用时。
Python · 可运行
python
def no_wraps(func):
def wrapper(*args, **kwargs):
func(*args, **kwargs) # 忘记 return
return wrapper
@no_wraps
def add(a, b):
return a + b
print(add(1, 2), add.__name__) # None wrapper对照
| 概念 | Python | TS/JS | Java |
|---|---|---|---|
| 语法 | @deco | @deco(TS 5 / 装饰器提案) | @Annotation |
| 本质 | 运行时替换函数/类 | 运行时替换/修饰 | 元数据,需框架或注解处理器解释 |
| 带参数 | @retry(times=3) | @retry(3) | @Retry(times = 3) |
| 典型用途 | 缓存、重试、注册、计时 | 同左 | Spring 等框架的配置 |
注意与 Java 注解的区别:Python 装饰器会立即执行并替换对象,不只是贴标签。
读代码
读到装饰器时,按两个问题理解:
- 它替换成了什么? 找到装饰器定义,看
wrapper在调用原函数前后做了什么(计时、重试、鉴权、缓存……)。 - 它在定义时做了什么? 注册型装饰器(如 Web 路由、Agent 框架的
@tool)在导入时把函数登记到某个表里。
注册型装饰器可以替代 minimal_agent.py 中手写的 TOOL_FUNCTIONS:
python
TOOL_FUNCTIONS = {}
def tool(func):
TOOL_FUNCTIONS[func.__name__] = func # 定义时登记
return func # 原样返回,不包装
@tool
def get_weather(city):
return f"{city}:晴,22°C"anthropic SDK 中的 client.messages 用的是 @cached_property:第一次访问时创建 Messages(self) 资源对象并缓存,之后直接返回同一个对象(10.4)。
练习
练习 1:注册型装饰器
实现装饰器 tool:把被装饰函数以其 __name__ 为键登记到全局字典 REGISTRY,并原样返回函数。
练习
REGISTRY = {}
def tool(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
快捷键:⌘/Ctrl + Enter 提交检查
练习 2:调用日志装饰器
实现装饰器 log_calls(log)(带参数):log 是一个列表;被装饰函数每次调用后,向 log 追加 (函数名, kwargs 字典, 返回值),并返回原返回值。使用 functools.wraps。
练习
import functools
def log_calls(log):
def decorator(func):
return func
return decorator
快捷键:⌘/Ctrl + Enter 提交检查
小结
@deco等价于f = deco(f);带参数时f = deco(参数)(f)。- 模板:闭包
wrapper(*args, **kwargs)+functools.wraps+ 记得return。 - 多个装饰器从下往上应用;装饰器在定义(导入)时执行。
- 读装饰器:问“替换成了什么”和“定义时做了什么”。