本节目标:看穿
@deco只是f = deco(f)的语法糖,掌握functools.wraps、带参数装饰器、类装饰器与方法装饰器,并能用lru_cache、singledispatch解决真实问题。
适用版本:Python 3.12+(实测 3.14.6)
8.3 装饰器原理与实战
第 4 章讲过「函数是一等对象」——能赋值、能传参、能返回。装饰器就是这条性质的直接产物:它是一个「接收函数、返回函数」的高阶函数,用来在不改动原函数体的前提下,给它追加行为。
装饰器就是「函数 → 函数」
先不写 @,看最朴素的形态:
def logged(func):
def wrapper(*args, **kwargs):
print(f" call {func.__name__}{args}")
return func(*args, **kwargs)
return wrapper
def add(a, b):
return a + b
add = logged(add) # 用 wrapper 替换掉原函数
print("add(2,3) =", add(2, 3))
# call add(2, 3)
# add(2,3) = 5
logged 拿到 add,返回一个新的 wrapper,并把它绑定回同名变量。从此 add 这个名字指向的就是带日志的版本。@ 语法只是把「传进去、再赋值回来」这一步写得更简洁:
@logged
def add(a, b):
return a + b
# 完全等价于 add = logged(add)
一句话总结:@deco 等价于 f = deco(f)。理解这一点,后面所有花样都是它的组合。
为什么必须用 functools.wraps
wrapper 返回后,原函数的身份信息(__name__、__doc__、参数签名)都会丢失——因为外部看到的已经是 wrapper 了。看对比:
import functools, inspect
def bad_deco(func):
def wrapper(*a, **k):
return func(*a, **k)
return wrapper
def good_deco(func):
@functools.wraps(func)
def wrapper(*a, **k):
return func(*a, **k)
return wrapper
@bad_deco
def greet(name):
"""打招呼"""
return f"hi {name}"
@good_deco
def greet2(name):
"""打招呼"""
return f"hi {name}"
print(greet.__name__, greet.__doc__) # wrapper None
print(greet2.__name__, greet2.__doc__) # greet2 打招呼
print(inspect.signature(greet)) # (*a, **k)
print(inspect.signature(greet2)) # (name)
不加 wraps 时,__name__ 变成 wrapper、__doc__ 变成 None、签名变成 (*a, **k)——这会破坏 IDE 提示、文档生成、pickle 乃至依赖签名的框架。@functools.wraps(func) 是装饰器的强制项,不是可选项。
带参数的装饰器
如果装饰器本身要接收参数(比如「重复几次」),就需要三层嵌套:最外层收参数,中间层收函数,最内层是真正的 wrapper:
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*a, **k):
return [func(*a, **k) for _ in range(times)]
return wrapper
return decorator
@repeat(3)
def say(msg):
return msg
print(say("hi")) # ['hi', 'hi', 'hi']
@repeat(3) 的执行顺序是:先算 repeat(3) 得到 decorator,再用 decorator 装饰 say。所以 @ 后面那个表达式会先被求值。理解了三层结构,鉴权、重试、超时这类「带配置」的装饰器就都能写了。
类装饰器与实例方法装饰器
装饰器的目标不只是普通函数,也可能是类或方法。类装饰器接收一个类、返回一个类:
def add_repr(cls):
cls.describe = lambda self: f"I am {type(self).__name__}"
return cls
@add_repr
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
print(Point(1, 2).describe()) # I am Point
dataclass(第 6 章)本质上就是一个功能强大的类装饰器——它在类定义完成后动态补上 __init__、__repr__、__eq__。
实例方法装饰器则要注意:被装饰的方法第一个参数是 self,wrapper 必须原样转发:
def trace_method(func):
@functools.wraps(func)
def wrapper(self, *a, **k):
print(f" call {func.__name__} on {type(self).__name__}")
return func(self, *a, **k)
return wrapper
class Calc:
@trace_method
def double(self, x):
return x * 2
print(Calc().double(5))
# call double on Calc
# 10
如果 wrapper 只写 def wrapper(*a, **k),self 会被当作普通参数塞进 a,调用 func(self, ...) 时就会错位——所以方法装饰器的 wrapper 必须显式声明 self。
functools.lru_cache / cache:装饰器解决真实问题
标准库里最常用的装饰器是 functools.lru_cache(带容量上限)和 functools.cache(无上限,等价于 lru_cache(maxsize=None))。它们把「函数调用 → 结果」缓存起来,重复调用直接命中:
calls = []
@functools.lru_cache(maxsize=None)
def fib(n):
calls.append(n)
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print("fib(30) =", fib(30)) # fib(30) = 832040
print("实际计算次数:", len(calls)) # 实际计算次数: 31
print(fib.cache_info())
# CacheInfo(hits=28, misses=31, maxsize=None, currsize=31)
朴素的递归 fib(30) 要算上百万次,加了缓存后只真正计算了 31 次——这就是**记忆化(memoization)**的威力。但缓存有一个硬约束:参数必须可哈希,因为它要用参数做字典的 key:
@functools.cache
def total(items):
return sum(items)
print(total((1, 2, 3))) # 6
try:
total([1, 2, 3])
except TypeError as e:
print(e) # unhashable type: 'list'
列表不可哈希,所以传列表会直接报错;要缓存列表参数,得先转成元组。此外要小心:缓存会一直持有结果和参数引用,无限增长可能吃光内存——长跑服务里优先用带 maxsize 的 lru_cache。
functools.singledispatch:按类型分派
functools.singledispatch 把「根据第一个参数的类型选择实现」包装成装饰器,避免一长串 isinstance 判断:
@functools.singledispatch
def render(value):
return f"unknown: {value!r}"
@render.register
def _(value: int):
return f"int -> {value}"
@render.register
def _(value: list):
return "list -> " + ", ".join(map(str, value))
print(render(42)) # int -> 42
print(render([1, 2, 3])) # list -> 1, 2, 3
print(render("hi")) # unknown: 'hi'
没有匹配类型时走被 @singledispatch 装饰的那个「默认实现」。这是多分派在标准库里最轻量的落地,做序列化、格式化、AST 访问时非常好用。
多个装饰器的执行顺序
当多个装饰器叠在一起,记住一句话:包装自下而上,执行自上而下。
def deco_a(func):
print(" apply A")
def wrapper(*a, **k):
print(" enter A")
r = func(*a, **k)
print(" exit A")
return r
return wrapper
def deco_b(func):
print(" apply B")
def wrapper(*a, **k):
print(" enter B")
r = func(*a, **k)
print(" exit B")
return r
return wrapper
@deco_a
@deco_b
def hello():
print(" hello")
hello()
# apply B
# apply A
# enter A
# enter B
# hello
# exit B
# exit A
应用阶段(装饰时)从下往上:先 apply B 再 apply A,因为 @deco_a 装饰的是 deco_b(hello) 的结果。运行阶段(调用时)从上往下:A 先进入、B 后进入,退出时反序。想清楚这条顺序,堆叠装饰器就不会搞反。
装饰器在真实工程里的位置
装饰器不是语法玩具,它是「横切关注点」的标准载体:
| 场景 | 装饰器做什么 |
|---|---|
| 鉴权 / 权限 | 调用前检查用户角色,不满足直接抛异常 |
| 重试 | 捕获异常后按策略重试,成功即返回 |
| 计时 / 监控 | 用 time.perf_counter 包住调用,记录耗时 |
| 注册表 | 把函数按名字登记进全局字典,供后续按名分派 |
| 缓存 | lru_cache / 自定义缓存 |
| 日志 / 追踪 | 记录入参、出参、异常 |
重试是一个典型例子——它把「失败后重来」这段控制流从业务代码里抽走:
def retry(times=3):
def decorator(func):
@functools.wraps(func)
def wrapper(*a, **k):
for attempt in range(1, times + 1):
try:
return func(*a, **k)
except Exception as e:
print(f" attempt {attempt} failed: {e}")
raise # 重试耗尽,抛出最后一次异常
return wrapper
return decorator
state = {"n": 0}
@retry(times=3)
def flaky():
state["n"] += 1
if state["n"] < 3:
raise RuntimeError("not yet")
return "ok"
print("flaky ->", flaky())
# attempt 1 failed: not yet
# attempt 2 failed: not yet
# flaky -> ok
注册表则利用「装饰器在导入时就会执行」这一点,把函数自动收集起来:
HANDLERS = {}
def register(name):
def decorator(func):
HANDLERS[name] = func
return func
return decorator
@register("add")
def do_add(a, b):
return a + b
@register("mul")
def do_mul(a, b):
return a * b
print(sorted(HANDLERS)) # ['add', 'mul']
print(HANDLERS["add"](2, 3)) # 5
这种「注册表 + 装饰器」的模式在 Web 框架(路由表)、命令分发、插件系统里随处可见。计时装饰器同理:用 time.perf_counter 包住调用,并放进 try/finally,这样即使被装饰的函数抛异常,耗时也会照常记录。
小结
@deco只是f = deco(f)的语法糖;装饰器就是「接收函数、返回函数」的高阶函数。- 必须用
@functools.wraps(func),否则__name__、__doc__、签名全部丢失。 - 带参数的装饰器需要三层嵌套;类装饰器接收并返回类,方法装饰器的
wrapper必须显式带self。 functools.lru_cache/cache做记忆化,参数必须可哈希,且要警惕缓存无限增长。functools.singledispatch按第一个参数类型分派,替代冗长的isinstance分支。- 多个装饰器包装自下而上、执行自上而下。
- 鉴权、重试、计时、注册表、缓存是装饰器最常见的工程落点。
到这里,你已经掌握了 Python 函数式编程的两大支柱:生成器(惰性数据流)与装饰器(行为组合)。但它们有一个共同的短板——类型信息在运行时几乎不被检查。下一节 9.1 类型注解语法与 pyright / mypy 进入第 9 章,讲如何用类型注解把「参数与返回值是什么」写进代码,并让工具在运行前替你抓错。
阅读导航:上一节:8.2 生成器进阶:send / yield from 与惰性管道 · 下一节:9.1 类型注解语法与 pyright / mypy 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。