断点打在被装饰函数里跳不进去,是因为装饰器替换了原函数为wrapper,调试器默认无法穿透;需用functools.wraps保留原函数元信息,并在launch.json中设"justMyCode": false以完整跟踪调用链。

为什么断点打在被装饰函数里却跳不进去
因为 Python 装饰器本质是函数替换:装饰后,@decorator 修饰的函数名实际指向装饰器返回的新函数(比如 wrapper),原函数对象被包裹、隐藏。VSCode 默认调试时,断点绑定的是符号名(如 my_func),但运行时真正执行的是 wrapper,而 wrapper 内部调用原函数时,若没做特殊处理,调试器无法“穿透”到原始函数体。
用 functools.wraps 保留原函数元信息
这是最基础也最关键的一步。不加 @functools.wraps(func),__name__、__doc__、__code__ 全都指向 wrapper,VSCode 断点根本找不到原函数上下文。
实操建议:
- 所有自定义装饰器内部的
wrapper函数前必须加@functools.wraps(func) - 检查是否漏掉导入:
from functools import wraps - 如果用了第三方装饰器(如
@lru_cache或@dataclass),它们内部已自带wraps;但像手写的重试、日志类装饰器,极易遗漏
示例对比:
立即学习“Python免费学习笔记(深入)”;
# ❌ 不透明:断点打在 my_func 上,调试时停在 wrapper 里,看不到原函数代码
def log_calls(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper # 缺少 wraps!
<h1>✅ 透明:断点能直接停在原函数体内部</h1><p>def log_calls(func):
@wraps(func)
def wrapper(*args, *<em>kwargs):
print(f"Calling {func.<strong>name</strong>}")
return func(</em>args, **kwargs)
return wrapper
VSCode launch.json 需启用 justMyCode 并设为 false
默认情况下,VSCode 的 Python 调试器开启 "justMyCode": true,它会跳过标准库和非工作区代码——但装饰器里的 wrapper 属于“我的代码”,而原函数被包裹后,其执行帧可能被判定为“非用户代码”。设为 false 后,调试器不再过滤调用栈,能完整呈现 wrapper → 原函数 的逐层调用链,断点才可能命中原始函数体。
配置要点:
- 在
.vscode/launch.json的对应配置中,显式添加:"justMyCode": false - 仅对调试装饰器场景临时关闭;日常调试建议保持
true避免进太多底层帧 - 该设置不影响断点位置绑定,只影响是否单步进入/停靠
遇到 step into 卡在 wrapper 里出不来?试试手动 step over + 断点下移
即使做了上面两步,有时调试器仍会在 wrapper 内部卡住,尤其当装饰器逻辑复杂(含异步、协程、多层嵌套)时。step into 可能反复进入装饰器工具函数而非你的原函数。
更可靠的做法:
- 在
wrapper中调用原函数的那一行(通常是return func(*args, **kwargs))打一个断点 - F10
step over执行完这行,调试器会自动跳进原函数体(前提是functools.wraps已生效) - 如果原函数是生成器或协程,确保 VSCode Python 扩展版本 ≥ 2023.8,旧版对
yield/await包裹调试支持不稳
真正麻烦的不是写法,而是装饰器堆叠——比如 @cache + @log_calls + @retry,每层都要 wraps,漏一层就断点失效。别图省事写“裸 wrapper”。


















