前面 装饰器入门 讲了装饰器的基本形式:”接受函数,返回函数”。真实项目里的装饰器还有几种进阶形态:带参数装饰器 (如 @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 functoolsdef 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 functoolsdef 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 : return decorator return decorator(_func) @log def a (): print ("a" ) @log(level="DEBUG" ) def b (): print ("b" ) a() 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) 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)
好处:可以给”包装器”挂状态(比如计数)而不用闭包。
带参数的类装饰器:
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 ))
Python 内置的 @dataclass 就是最经典的”装饰类”——自动生成 __init__、__repr__、__eq__。
标准库里的一大明星装饰器:
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 ) @cache def fib (n ): ...fib(50 ) fib.cache_info() 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。
1 2 3 4 5 6 7 import functoolsdef 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 functoolsimport inspectdef keep_signature (func ): @functools.wraps(func ) def wrapper (*args, **kwargs ): return func(*args, **kwargs) 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 ) add(1 , 2 , z=100 )
修复 :wrapper 用 *args, **kwargs 通吃:
1 2 3 4 5 def deco (func ): def wrapper (*args, **kwargs ): return func(*args, **kwargs) return wrapper
十、多个装饰器叠加
等价 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()
十一、类方法装饰器: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 )) print (register._registry["mul" ](3 , 4 ))
十二、常见业务装饰器 权限校验 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 timefrom functools import wrapsdef 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()
十三、一个完整例子: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 """一个 mini 版的 Flask-like 路由装饰器。""" import functoolsfrom 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" ))
十四、常见陷阱
忘 @functools.wraps :func.__name__、docstring 丢失。
参数装饰器少一层嵌套 :@retry(3) 报错,因为 retry 直接被当装饰器。三层嵌套是标配。
wrapper 不用 *args, **kwargs :签名不匹配时报错。
类装饰器忘 update_wrapper :__name__ 变成类名而不是函数名。
@lru_cache 参数是 list/dict :TypeError。要 tuple 化。
循环里 def deco :会捕获循环变量。改用默认参数或 partial。
装饰类时想改 __init__ :改要小心,可能破坏原类。
十五、小结与延伸阅读
带参数装饰器 = 三层嵌套(装饰器工厂 → 装饰器 → wrapper);
类装饰器 = 实现 __call__ 的类;
装饰器也能装类;
用 *args, **kwargs 让 wrapper 通吃参数;
混合 “@deco / @deco(…)” 用 _func=None + 关键字参数技巧;
functools.lru_cache/cache 是最常用的内置装饰器;
记得 functools.wraps 保留元信息。
延伸阅读:
下一篇 正则表达式 re 模块实战 我们讲字符串处理的终极武器——正则。