装饰器进阶:带参数装饰器与类装饰器

前面 装饰器入门 讲了装饰器的基本形式:”接受函数,返回函数”。真实项目里的装饰器还有几种进阶形态:带参数装饰器(如 @retry(times=3))、类装饰器(用类作为装饰器)、装饰整个类保留 signature 的装饰器(用 functools.wraps+inspect)。这一篇把它们讲清。

一、带参数装饰器:三层嵌套

前面提过,@retry(times=3) 的实现结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import functools

def retry(times=3, delay=1):
"""外层:接收装饰器参数,返回真正的装饰器。"""
def decorator(func):
"""真正的装饰器:接收函数,返回包装。"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
"""包装:真正执行的地方。"""
for i in range(times):
try:
return func(*args, **kwargs)
except Exception as e:
if i == times - 1:
raise
print(f"[{i+1}] 失败:{e}")
import time; time.sleep(delay)
return wrapper
return decorator


@retry(times=3, delay=0.5)
def flaky():
import random
if random.random() < 0.7:
raise RuntimeError("挂了")
return "OK"

核心思想

  • retry(3, 0.5) 先被调用——返回 decorator;
  • decorator 再作为装饰器包装 flaky——返回 wrapper。

@retry(times=3, delay=0.5) = retry(3, 0.5)(flaky) = decorator(flaky)。

二、装饰器同时支持”带参数”和”不带参数”

想让 @log@log(level="INFO") 都行?看起来矛盾——一个是接收函数,一个是接收参数。技巧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import functools

def log(_func=None, *, level="INFO"):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"[{level}] 调用 {func.__name__}")
return func(*args, **kwargs)
return wrapper

if _func is None:
# 用 @log(level=...) 形式:只传关键字参数,_func 是 None
return decorator
# 用 @log 形式:_func 是被装饰的函数
return decorator(_func)


@log
def a():
print("a")


@log(level="DEBUG")
def b():
print("b")


a() # [INFO] 调用 a → a
b() # [DEBUG] 调用 b → b

规则:把装饰器参数改为仅关键字参数(用 *),第一个位置参数留给”可能的被装饰函数”。

三、类作为装饰器

用类做装饰器就是”实现 __call__ 的对象”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func) # 类似 wraps
self.func = func
self.count = 0

def __call__(self, *args, **kwargs):
self.count += 1
return self.func(*args, **kwargs)


@CountCalls
def greet(name):
print(f"你好,{name}")


greet("咖飞")
greet("小王")
print(greet.count) # 2

好处:可以给”包装器”挂状态(比如计数)而不用闭包。

带参数的类装饰器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Retry:
def __init__(self, times=3):
self.times = times

def __call__(self, func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(self.times):
try:
return func(*args, **kwargs)
except Exception:
pass
raise
return wrapper


@Retry(times=5)
def foo():
...

四、装饰整个类

装饰器不仅能装函数,还能装类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def add_repr(cls):
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 Point:
def __init__(self, x, y):
self.x, self.y = x, y


print(Point(3, 4)) # Point(x=3, y=4)

Python 内置的 @dataclass 就是最经典的”装饰类”——自动生成 __init____repr____eq__

五、functools.lru_cache 与 cache

标准库里的一大明星装饰器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from functools import lru_cache, cache

@lru_cache(maxsize=128)
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)


# Python 3.9+ 无界版
@cache
def fib(n): ...


fib(50) # 秒出,不用 lru_cache 要算好久
fib.cache_info() # CacheInfo(hits=X, misses=Y, maxsize=128, currsize=Z)
fib.cache_clear() # 清缓存

要求:参数必须可哈希——不能传 list/dict/set。

六、方法装饰器

装饰类的方法要注意 self——用 *args 通吃:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def logged(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"[LOG] {func.__name__} 被调")
return func(*args, **kwargs)
return wrapper


class Service:
@logged
def run(self):
print("running")


Service().run()

*args 会包含 self

七、functools.wraps 究竟做了什么

1
2
3
4
5
6
7
import functools

def deco(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

@functools.wraps(func) 相当于把 wrapper 的:

  • __name__ 设为 func.__name__
  • __doc__ 设为 func.__doc__
  • __module____qualname____wrapped__ 等;
  • __dict__ 更新。

好处:调试器、日志、Sphinx 文档、类型检查都能看到原函数信息。

八、保留 signature

标准 wraps 保留元信息,但 signature(参数列表)不保留——如果你想让 IDE 补全参数:

1
2
3
4
5
6
7
8
9
10
import functools
import inspect

def keep_signature(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
# 手动设 signature
wrapper.__signature__ = inspect.signature(func)
return wrapper

实际项目里 functools.wraps 已经够用,除非你用非常严格的类型系统。

九、装饰器的”参数被吃掉”陷阱

看这段:

1
2
3
4
5
6
7
8
9
10
11
12
13
def deco(func):
def wrapper(x, y):
return func(x + 1, y + 1)
return wrapper


@deco
def add(x, y, z=10):
return x + y + z


add(1, 2) # 5
add(1, 2, z=100) # ❌ wrapper 不接受 z

修复:wrapper 用 *args, **kwargs 通吃:

1
2
3
4
5
def deco(func):
def wrapper(*args, **kwargs):
# 处理 args/kwargs
return func(*args, **kwargs)
return wrapper

十、多个装饰器叠加

1
2
3
4
@a
@b
@c
def f(): ...

等价 f = a(b(c(f)))。执行时 c 最先运行,a 最外层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def log(func):
@functools.wraps(func)
def w(*args, **kw):
print(f"log begin {func.__name__}")
r = func(*args, **kw)
print(f"log end {func.__name__}")
return r
return w

def timed(func):
@functools.wraps(func)
def w(*args, **kw):
print(f"timer start")
r = func(*args, **kw)
print(f"timer end")
return r
return w


@log
@timed
def work():
print("working")


work()
# log begin work
# timer start
# working
# timer end
# log end work

十一、类方法装饰器:classmethod/staticmethod/property

前面 类方法、静态方法与 property 讲过。这里补充:这些也是装饰器——只是内置的。你可以自己写”内置装饰器风格”的东西:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class register:
"""把方法注册到某个字典。"""
_registry = {}

def __init__(self, name):
self.name = name

def __call__(self, func):
type(self)._registry[self.name] = func
return func


@register("add")
def do_add(a, b):
return a + b


@register("mul")
def do_mul(a, b):
return a * b


print(register._registry["add"](3, 4)) # 7
print(register._registry["mul"](3, 4)) # 12

十二、常见业务装饰器

权限校验

1
2
3
4
5
6
7
8
9
def require_role(role):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if role not in current_user["roles"]:
raise PermissionError()
return func(*args, **kwargs)
return wrapper
return decorator

参数校验

1
2
3
4
5
6
7
8
def validate_positive(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for a in args:
if isinstance(a, (int, float)) and a < 0:
raise ValueError(f"参数不能为负:{a}")
return func(*args, **kwargs)
return wrapper

结果缓存(带 TTL)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import time
from functools import wraps

def ttl_cache(ttl=60):
def decorator(func):
cache = {}
@wraps(func)
def wrapper(*args, **kwargs):
key = (args, tuple(sorted(kwargs.items())))
now = time.time()
if key in cache:
val, ts = cache[key]
if now - ts < ttl:
return val
val = func(*args, **kwargs)
cache[key] = (val, now)
return val
return wrapper
return decorator

单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def singleton(cls):
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance


@singleton
class Config:
pass


Config() is Config() # True

十三、一个完整例子:API 端点装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# webapi.py
"""一个 mini 版的 Flask-like 路由装饰器。"""

import functools
from typing import Callable


class App:
def __init__(self):
self.routes: dict[tuple[str, str], Callable] = {}

def route(self, path: str, method: str = "GET"):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"[{method}] {path}")
return func(*args, **kwargs)
self.routes[(method, path)] = wrapper
return wrapper
return decorator

def dispatch(self, method: str, path: str):
handler = self.routes.get((method, path))
if handler is None:
return 404, "Not Found"
return 200, handler()


app = App()


@app.route("/")
def home():
return "首页"


@app.route("/hello", method="POST")
def hello():
return "hi 咖飞"


if __name__ == "__main__":
print(app.dispatch("GET", "/"))
print(app.dispatch("POST", "/hello"))
print(app.dispatch("GET", "/miss"))

十四、常见陷阱

  1. @functools.wrapsfunc.__name__、docstring 丢失。
  2. 参数装饰器少一层嵌套@retry(3) 报错,因为 retry 直接被当装饰器。三层嵌套是标配。
  3. wrapper 不用 *args, **kwargs:签名不匹配时报错。
  4. 类装饰器忘 update_wrapper__name__ 变成类名而不是函数名。
  5. @lru_cache 参数是 list/dict:TypeError。要 tuple 化。
  6. 循环里 def deco:会捕获循环变量。改用默认参数或 partial。
  7. 装饰类时想改 __init__:改要小心,可能破坏原类。

十五、小结与延伸阅读

  • 带参数装饰器 = 三层嵌套(装饰器工厂 → 装饰器 → wrapper);
  • 类装饰器 = 实现 __call__ 的类;
  • 装饰器也能装类;
  • *args, **kwargs 让 wrapper 通吃参数;
  • 混合 “@deco / @deco(…)” 用 _func=None + 关键字参数技巧;
  • functools.lru_cache/cache 是最常用的内置装饰器;
  • 记得 functools.wraps 保留元信息。

延伸阅读:

下一篇 正则表达式 re 模块实战 我们讲字符串处理的终极武器——正则。