Python 装饰器实战:从 @wraps 到带参数工厂的四个台阶
Python 装饰器实战:从 @wraps 到带参数工厂的四个台阶
装饰器是 Python 面试和工程代码里的高频考点,但多数教程停在"打印时间"的示例。本文按四个台阶把装饰器讲透,每个台阶配一段可直接运行的最小代码。
台阶一:最朴素的包装器
函数是一等对象,装饰器本质就是"接收函数、返回新函数":
def log_call(func):
def wrapper(*args, **kwargs):
print(f"call {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_call
def add(a, b):
return a + b
这里 @log_call 只是 add = log_call(add) 的语法糖。但此时 add.__name__ 已经变成了 wrapper——这是第一个坑。
台阶二:functools.wraps 修复元信息
被包装后的函数丢失了名字、文档字符串和签名,调试器、序列化器、文档生成器都会被骗:
from functools import wraps
def log_call(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@wraps(func) 把 __name__、__doc__、__wrapped__ 等属性拷回来,inspect.signature 也能还原参数列表。写任何装饰器都应默认带上它,这是工程底线。
台阶三:带参数的装饰器 = 三层嵌套
@retry(times=3) 这种写法,装饰器本身要接收参数,于是多了一层"工厂":
from functools import wraps
import time
def retry(times=3, delay=1):
def decorator(func):
@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
time.sleep(delay)
return wrapper
return decorator
@retry(times=3, delay=2)
def fetch(url):
...
理解关键:@retry(times=3) 先执行 retry(times=3) 返回真正的装饰器,再装饰函数。三层嵌套 = 工厂 → 装饰器 → 包装器。
台阶四:保留类型与 IDE 提示
直接返回 wrapper 会让类型检查器认为返回值类型丢失。用 ParamSpec 可以把签名"透传":
from typing import Callable, ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def log_call(func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
return func(*args, **kwargs)
return wrapper
P.args / P.kwargs 绑定原函数的参数形态,pyright/mypy 不再报类型漂移,IDE 的跳转与补全也保持正常。
常见坑清单
- 忘记 @wraps:
__name__变 wrapper,日志和监控全部串名。 - 装饰类方法时丢 self:包装器里
*args第一个就是 self,别把它当业务参数吃掉。 - 装饰器在 import 时执行:模块导入的副作用要克制,别在装饰器里做重活。
- 对 async 函数用了同步包装:需要
async def wrapper并 await,否则拿到的是 coroutine 而不是结果。
小结
装饰器的四个台阶:会写包装器 → 记得 @wraps → 会写带参工厂 → 会用 ParamSpec 保类型。把这段 retry 工厂收进工具库,比每次手写 try/sleep 干净得多。
- 点赞
- 收藏
- 关注作者
评论(0)