前言
装饰器(decorator)常被讲得很玄,但它的底层机制只有一句话:函数是对象,所以函数可以被当作参数传进另一个函数,也可以被另一个函数返回。装饰器就是把这两件事组合起来的固定套路。
初学者最容易卡在三处:
- 记不住
@到底做了什么。记住等价写法f = deco(f)就够,@只是这个赋值的语法糖。 - 写不出「带参数的装饰器」。其实只是多包一层,从两层变三层。
- 不知道装饰器还能装饰类,也不知道装饰器可以写成类。
本文按「本质 → 函数装饰器 → 带参装饰器 → 类装饰器 → 装饰类 → 实战要点」的顺序推进。所有示例按 Python 3.8 就能跑的写法给出;需要更高版本的语法会单独标注。
一、本质:@只是赋值语法糖
# 适用于 Python 3.8+
def deco(func):
print("deco 收到:", func.__name__)
return func # 什么都不做,原样返回
@deco
def hello():
return "hi"
# 上面三行等价于:
# def hello(): return "hi"
# hello = deco(hello)
print(hello())deco 收到: hello
hi两个必须记住的结论:
- 装饰器在函数定义时执行,不是调用时执行。所以
print在导入/定义阶段就打印了。 - 装饰器必须返回可调用对象。返回
None的话,hello这个名字就变成None了。
二、函数装饰器与functools.wraps
一个真正做事的装饰器要返回一个「包装函数」,在调用原函数前后插入逻辑:
# 适用于 Python 3.8+
import functools
def logged(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("begin", func.__name__)
result = func(*args, **kwargs)
print("end", func.__name__)
return result
return wrapper
@logged
def add(a, b):
"""把两个数相加。"""
return a + b
print(add.__name__) # add
print(add.__doc__) # 把两个数相加。如果去掉@functools.wraps(func),add.__name__会变成wrapper、add.__doc__会变成None。functools.wraps是functools.update_wrapper的便捷形式,按官方文档,它默认拷贝__module__、__name__、__qualname__、__annotations__、__doc__(Python 3.12 起还默认拷贝__type_params__),并给包装函数挂上__wrapped__指回原函数。
三、带参数的装饰器:从两层到三层
想写@retry(3)这种形式,就要多一层:最外层收「配置参数」,中间层是真正的装饰器,最内层是包装函数。
# 适用于 Python 3.8+
import functools
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say(msg):
print(msg)
say("hi")hi
hi
hi判断层数的口诀:返回值用不用再被调用一次。repeat(3)返回的是decorator,decorator(say)返回的是wrapper,真正被调用三次的是wrapper。
四、把装饰器写成类
只要一个类实现了__call__,它的实例就是可调用对象,因此也能当装饰器。用类的好处是状态存在实例属性里,比闭包直观。
# 适用于 Python 3.8+
import functools
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
return self.func(*args, **kwargs)
@CountCalls
def ping():
return "pong"
ping()
ping()
print(ping.count) # 2
print(ping.__name__) # ping这里用functools.update_wrapper(self, func)而不是functools.wraps,因为wraps是给函数用的装饰器语法,而self是实例。类实例有__dict__,update_wrapper会正常把元信息写进去。
五、装饰类
装饰器也能加在class上方。此时被装饰的是类对象,装饰器通常返回一个加工过的类(或一个函数)。这是单例、注册表等模式的常见实现方式。
# 适用于 Python 3.8+
import functools
def singleton(cls):
"""让一个类只产生一个实例。"""
instances = {}
@functools.wraps(cls, updated=())
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class Config:
def __init__(self):
self.values = {}
a = Config()
b = Config()
print(a is b) # True这里的updated=()是故意的:functools.wraps默认会把原对象的__dict__更新到包装函数上,对类来说就是搬类命名空间,没什么意义还容易出错,所以传空元组跳过这一步。
六、叠加顺序与标准库里的现成装饰器
多个装饰器叠加时,从下往上执行,最靠近def的最先被应用:
# 适用于 Python 3.8+
def first(func):
def w(*a, **k):
print("first 前")
r = func(*a, **k)
print("first 后")
return r
return w
def second(func):
def w(*a, **k):
print("second 前")
r = func(*a, **k)
print("second 后")
return r
return w
@first
@second
def work():
print("work")
work()first 前
second 前
work
second 后
first 后也就是first(second(work))。叠加时要留意「谁包在外面」,异常处理、计时这类装饰器的内外顺序会直接影响结果。
标准库里已经有一批打磨好的装饰器,优先用它们而不是自己造:
| 装饰器 | 来源 | 作用 |
|---|
@property | 内置 | 把方法变成属性访问 |
@classmethod/@staticmethod | 内置 | 改变首个隐式参数 |
@functools.lru_cache | functools | 带容量上限的缓存(3.2+) |
@functools.cache | functools | 无容量上限的缓存(3.9+) |
@functools.total_ordering | functools | 补齐比较方法 |
@functools.singledispatch | functools | 按第一个参数类型分派 |
@contextlib.contextmanager | contextlib | 用生成器写上下文管理器 |
@abc.abstractmethod | abc | 标记抽象方法 |
七、实战要点:装饰器加在方法上
把普通函数装饰器加在实例方法上时,self会被*args收进去,通常不用特殊处理:
# 适用于 Python 3.8+
import functools
def logged(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("calling", func.__name__)
return func(*args, **kwargs)
return wrapper
class Cart:
def __init__(self):
self.items = []
@logged
def add(self, name):
self.items.append(name)
return len(self.items)
c = Cart()
print(c.add("apple"))calling add
1但如果装饰器里要读方法名或做类型判断,就要意识到args[0]是self。另外@classmethod、@staticmethod与其它装饰器叠加时顺序很重要:@classmethod/@staticmethod通常要放在最外层(最上面),让它最后被应用,才能正确地把底层函数包成描述符。
常见坑点
- 装饰器没返回可调用对象
❌def deco(f): print(f)没有return,函数名变成None✅ 每个装饰器都要return包装函数(或原函数、或加工后的类)
- 忘加
functools.wraps
❌ 所有被装饰函数都叫wrapper,文档字符串也没了 ✅ 包装函数上加@functools.wraps(func)
- 带参数的装饰器只写两层
❌@retry(3)而retry只接受一个参数,导入就报错 ✅ 三层结构:配置层 → 装饰器层 → 包装层
- 以为装饰器在调用函数时才执行
❌ 在装饰器顶层写初始化逻辑,以为会随每次调用重复 ✅ 顶层代码在定义时只执行一次;要每次执行就写进wrapper
- 叠加顺序记反
❌ 以为离def远的先执行 ✅ 自下而上套用,等价于outer(inner(func))
- 类装饰器里用
functools.wraps包类
❌functools.wraps(cls)把类命名空间塞进函数__dict__✅ 用functools.wraps(cls, updated=())跳过__dict__更新
- 类装饰器忘了实现
__call__
❌@MyDeco而MyDeco实例不可调用,调用时报TypeError✅ 类要实现__call__才配当装饰器
- 把
@classmethod放在装饰器链的中间
❌@logged写在@classmethod上面,cls绑定异常 ✅@classmethod/@staticmethod放最外层(最上面)
总结
| 阶段 | 形式 | 关键点 |
|---|
| 入门 | 函数装饰器 | f = deco(f),返回包装函数 |
| 进阶 | 加functools.wraps | 保住元信息与__wrapped__ |
| 进阶 | 带参数的装饰器 | 三层结构 |
| 精通 | 类装饰器 | 实现__call__,状态存实例属性 |
| 精通 | 装饰类 | 装饰class,可返回类或函数 |
| 精通 | 叠加 | 自下而上,注意内外顺序 |
装饰器没有「高级技巧」这一说,全部能力都来自「函数是一等对象」这一条。把f = deco(f)记牢、把functools.wraps变成条件反射、把带参装饰器的三层结构练熟,剩下的就是按需求往包装函数里填逻辑而已。