Python 装饰器实战:从 @wraps 到带参数工厂的四个台阶

举报
yd_257742204 发表于 2026/11/06 07:53:26 2026/11/06
【摘要】 装饰器四个台阶:包装器、@wraps 保元信息、带参数的重试工厂、ParamSpec 保类型提示,附四个常见坑。Python

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 的跳转与补全也保持正常。

常见坑清单

  1. 忘记 @wraps:__name__ 变 wrapper,日志和监控全部串名。
  2. 装饰类方法时丢 self:包装器里 *args 第一个就是 self,别把它当业务参数吃掉。
  3. 装饰器在 import 时执行:模块导入的副作用要克制,别在装饰器里做重活。
  4. 对 async 函数用了同步包装:需要 async def wrapper 并 await,否则拿到的是 coroutine 而不是结果。

小结

装饰器的四个台阶:会写包装器 → 记得 @wraps → 会写带参工厂 → 会用 ParamSpec 保类型。把这段 retry 工厂收进工具库,比每次手写 try/sleep 干净得多。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。