VS Code 调试 Python 全攻略:launch.json、断点与排查实战 调试这件事说穿了就是把程序的黑箱掀开一条缝看它在你以为它会走 A 路径的时候到底拐去了哪个岔口。用 VS Code 调试 Python是我这几年打交道最多的一套组合编辑器轻、启动快、插件生态成熟而且配置一旦跑通后面几乎不用再折腾。很多人卡在第一步——装完 VS Code、装完 Python按 F5 却弹出一堆看不懂的报错或者断点打上去变成一个灰色空心圈程序呼呼跑完根本不带停的。这篇就围绕VS Code 调试 Python这条主线把环境搭建、launch.json 配置、断点技巧、多场景落地和踩坑排查从头到尾讲一遍。不管你是刚写完第一个 for 循环的新手还是已经在写接口脚本、数据处理、后台服务的老手都能从里面找到能直接抄的配置和能立刻用的排查思路。1. 环境搭建把 VS Code 和 Python 接上线环境这一步看起来最没技术含量实际上是后面九成调试问题的根源。解释器选错、虚拟环境没激活、插件没装全都会表现为断点不生效模块找不到这类看起来很高深的症状。所以这一章我写得细一点把每个动作背后的理由讲清楚你照着做一遍后面就少走很多弯路。1.1 解释器安装与 PATH 这件小事Python 装哪个版本不用太纠结3.10 到 3.12 之间挑一个稳定的就行新项目别用太老的版本语法和类型提示会跟不上。Windows 上从官网下载安装包安装界面底部有两个勾选项其中Add python.exe to PATH必须勾上。这个勾选决定了一件事你在任意终端敲python的时候系统能不能找到它。没有勾你就得自己把安装目录下的 Scripts 和根目录手动加进环境变量麻烦且容易漏。装完打开一个新的命令行窗口敲下面两行验证python --version pip --version能打印出版本号就算过关。macOS 上可以用官网的 pkg 包也可以用 Homebrew 的brew install python后者省心一些。Linux 发行版一般自带 Python但建议单独装一个新版本别动系统自带的那一个因为系统里很多工具依赖它换版本容易出连锁反应。这里有个实战经验机器上同时存在多个 Python 版本时Windows 用py -3.11 --version这种带版本号的方式启动更稳妥macOS 和 Linux 则可以用python3.11。VS Code 底部状态栏会显示当前选中的解释器版本养成每次开新项目先瞄一眼的习惯能省下大量为什么我 pip 装了这个包却 import 不到的困惑。1.2 虚拟环境让每个项目各过各的全局环境里装包是新手最容易踩的坑。A 项目要 requests 2.25B 项目要 requests 2.31装到全局就互相覆盖最后两个项目都有诡异 bug。虚拟环境就是为了解决这个它给每个项目一份独立的 site-packages装包互不干扰。在项目根目录执行# 创建虚拟环境目录名一般叫 .venv python -m venv .venv激活方式分平台# Windows .venv\Scripts\activate # macOS / Linux source .venv/bin/activate激活成功后命令行前面会出现(.venv)的标记。然后在 VS Code 里按CtrlShiftP调出命令面板输入Python: Select Interpreter选中.venv目录里那个解释器。这一步是整套流程里最关键的绑定动作选错解释器你后面 pip 装的包、打的断点都可能对不上号。我个人的习惯是把.venv加进.gitignore团队协作时别人拉代码后自己建一份虚拟环境即可不要把整个虚拟环境目录提交上去几百兆的东西提交上去纯属给仓库添堵。依赖用requirements.txt或者pyproject.toml管理一条pip install -r requirements.txt就能复现环境。1.3 插件三件套与中文界面VS Code 本身是个通用编辑器调试 Python 的能力全靠插件补。必装的就三个Python微软官方提供调试、测试、环境管理、Pylance语言服务器负责补全、跳转、类型检查、Python Debugger新一代调试适配器底层是 debugpy。前两个装好之后基本会自动带上第三个如果没带上就手动搜一下补装。插件装完界面上会有几个明显变化底部状态栏出现解释器版本右键代码有Run Python File选项左侧活动栏多出运行和调试的三角图标。如果你英文界面用着别扭可以装中文语言包。命令面板里搜索Configure Display Language选择简体中文重启编辑器即可。装完语言包后调试面板里的按钮会变成继续单步跳过单步进入这些中文对新手理解调试状态流很有帮助。不过我建议核心概念还是记住英文原名比如 Step Over、Step Into、Step Out因为网上绝大多数资料和报错信息都是英文的术语对不上会更麻烦。另外有几个提升幸福感的设置可以顺手配上打开settings.json加上{ files.autoSave: onFocusChange, editor.formatOnSave: true, python.analysis.typeCheckingMode: basic, editor.rulers: [88] }自动保存配合断点调试特别香因为断点提示过文件已修改未保存的人都知道程序跑的是旧代码是什么体验。1.4 第一次按 F5跑通 Hello 级调试建一个demo.pydef calc_total(items): total 0 for i, price in enumerate(items): total price return total if __name__ __main__: prices [12.5, 30.0, 8.8, 45.2] result calc_total(prices) print(f总计: {result})在total price这一行的行号左边点一下出现一个红点这就是断点。按 F5顶部会弹出选择调试器第一次选Python File。程序启动后会在断点处停住当前那一行高亮左侧面板随之刷新VARIABLES 显示当前作用域里的变量能看到 i、price、total 的实时值WATCH 是你手动添加的监视表达式CALL STACK 是调用栈能从内层函数一路看到外层上方悬浮的调试工具条有六个按钮对应继续、单步跳过、单步进入、单步跳出、重启、停止把这几个快捷键记住用起来会顺手很多F5继续运行到下一个断点F9在当前行开关断点F10单步跳过不进入函数内部F11单步进入进到函数里ShiftF11单步跳出ShiftF5停止调试CtrlShiftF5重启调试。第一次能停住、能看到变量环境就算通了。后面所有复杂配置都是在这次成功的基础上叠加。2. launch.json 到底在管什么核心参数逐个拆很多教程跳过 launch.json 直接教按 F5结果读者一到要给脚本传参数要调试 Flask 服务就懵了。这个文件本质上是把调试启动方式固化下来让同一套参数可复用、可提交进版本库、可被团队共享。理解它调试能力会立刻上一个台阶。2.1 它从哪来又要放到哪打开左侧运行和调试面板点击创建 launch.json 文件选择Python FileVS Code 会在项目根目录下生成.vscode/launch.json。这个路径不要随便改编辑器只认.vscode目录。文件顶层有一个version字段目前固定是0.2.0不用管它。真正起作用的是configurations数组里面每一个对象就是一套调试方案左上角的下拉框会在这些方案之间切换。之所以要把它落成文件而不是每次都手动填是因为调试参数往往和项目结构绑定。比如工作目录在哪、环境变量怎么传、入口脚本是谁这些一旦固定团队里每个人拉下代码就能直接按 F5不用口口相传你先 cd 到某个目录再跑。这比自己记一堆命令靠谱得多。2.2 一份可以直接抄的通用模板下面这份配置适配绝大多数单文件脚本和小型项目我把每个字段都注释清楚了{ version: 0.2.0, configurations: [ { name: Python: 当前文件, type: debugpy, request: launch, program: ${file}, console: integratedTerminal, cwd: ${workspaceFolder}, args: [], env: { PYTHONPATH: ${workspaceFolder} }, envFile: ${workspaceFolder}/.env, justMyCode: true, stopOnEntry: false } ] }逐条说明name是下拉框里显示的名字随便起但建议写清楚用途。type在新版插件里是debugpy老教程里写python两者目前都还能用但新项目建议直接用debugpy。request取值launch表示启动一个新进程取值attach表示附加到已运行的进程这是两种完全不同的调试模式。program是入口脚本${file}代表当前打开的文件改成${workspaceFolder}/main.py就固定跑主入口。args是命令行参数数组。env和envFile负责注入环境变量前者直接写后者从文件读读文件的方式更适合放密钥。justMyCode控制是否跳过库代码。stopOnEntry设成 true 会在程序第一行就停下适合想从头跟流程的场景。关于${}变量常用的有这几个${file}当前文件、${workspaceFolder}工作区根目录、${fileDirname}当前文件所在目录、${fileBasenameNoExtension}当前文件名去掉扩展名、${env:HOME}读取环境变量。用好它们同一份配置能在不同机器上正常工作不会因为绝对路径写死而失效。2.3 justMyCode、console、cwd 三个最容易配错的参数这三个参数我单独拎出来讲因为它们的默认值经常和人的直觉相反。先说justMyCode。默认值 true 的含义是只调试我写的代码第三方库和标准库内部会被自动跳过。好处是单步进入函数时不会被带进 requests 内部几万行代码里出不来效率极高。坏处是当你想确认是不是某个库内部出了问题、想把断点打进库源码时会发现断点是灰色的、根本停不住。这时候把它改成 false重启调试就能进库了。我的建议是日常保持 true只在专门排查库问题时临时关掉。再说console。它有三个取值internalConsole、integratedTerminal、externalTerminal。默认的internalConsole在调试控制台里输出缺点是不支持标准输入程序一旦执行到input()就会直接卡死你以为是代码死循环其实是控制台吃不下输入。脚本里用到input()、用到命令行交互、或者要看到带颜色的日志一律改成integratedTerminal也就是用集成终端跑交互体验和普通命令行一模一样。externalTerminal会弹出一个独立的系统终端窗口适合需要多窗口对照观察的场景。最后是cwd当前工作目录。这个参数决定了代码里所有相对路径的基准点。你的代码里写了open(data/input.csv)程序实际去哪个目录找这个文件就是由 cwd 决定的。默认情况下调试器的工作目录可能不是你项目根目录于是出现我明明在项目里跑得好好的一按 F5 就报文件找不到这种经典问题。统一把cwd设成${workspaceFolder}让相对路径都以项目根为基准能消灭绝大部分路径类报错。2.4 多配置并存与组合启动一个中等项目往往需要好几套调试方案平时跑主程序是一套跑测试是一套调试某个独立脚本又是一套。这些全都可以塞进同一个configurations数组下拉框切换即可互不干扰。更进阶的用法是compounds它能把多个配置打包成一个组合按一次 F5 同时启动多个进程。典型场景是后台服务加一个消费者进程两个进程要同时起来才能复现问题{ version: 0.2.0, configurations: [ { name: 服务端, type: debugpy, request: launch, program: ${workspaceFolder}/server.py, console: integratedTerminal }, { name: 消费者, type: debugpy, request: launch, program: ${workspaceFolder}/worker.py, console: integratedTerminal } ], compounds: [ { name: 全部启动, configurations: [服务端, 消费者] } ] }配置好之后调试下拉框里会多出全部启动这一项选中它按 F5两个进程会同时以调试模式拉起两个断点都能命中。这个功能在排查进程间通信、消息队列消费这类问题时特别好用比开两个编辑器窗口手动协调省事得多。3. 断点、变量、调用栈把调试器用到刀刃上环境通了、配置懂了接下来就是真正的技术活怎么用断点精准地把问题框住怎么用变量面板快速看清数据状态怎么用调用栈回溯到问题的源头。这一章是整篇的核心值得多花点时间。3.1 五种断点各管一段路大部分人只会用最基础的行断点其实 VS Code 支持好几种用对了效率差好几倍。行断点是一个红点程序运行到这一行就停。这是最常用的但循环里打行断点会遇到一个尴尬循环一万次你得按一万次 F5。这时候就需要第二种。条件断点是行断点的升级版。右键一个断点选择编辑断点在 Expression 里写条件比如i 500或者item.get(status) failed只有条件为真时才停。排查第几千条数据出错这类问题条件断点是唯一的选择。注意条件里不要写太复杂的表达式因为调试器要在每次循环都求值一遍写个 O(n) 的条件会给程序带来可观的开销反而拖慢排查。命中计数断点按次数触发可以设第 100 次命中时停下适合循环次数明确、想从某个固定点开始观察的场景。日志断点Logpoint非常值得单独说。它不改代码、不打断执行只在命中时往调试控制台打印一条消息。用法是右键断点选择添加日志点消息里可以用{}插值比如写i{i}, total{total}, item{item}。它比临时插print干净一万倍不用改源码、不用删代码、不用担心忘了清理。生产环境不方便打断点的时候日志断点就是救命稻草。函数断点不绑在某一行上而是绑定在一个函数名上可以在 BREAKPOINTS 面板点击加号输入模块名.函数名添加。适合函数被很多地方调用、你只想在它被调用的入口停一次的场景。五种断点组合起来基本能覆盖日常排查的绝大多数需求。3.2 变量面板、监视表达式与调试控制台的配合程序停在断点上左侧的 VARIABLES 面板会自动展开当前作用域的局部变量。列表里的对象可以一层层点开看嵌套结构。字典、列表、对象属性都能展开只是要注意展开很大的对象比如一个十万行的 DataFrame会很卡调试器需要序列化整个对象结构。遇到这种情况别急着点开用下面的方法。监视WATCH面板是可以自己定义表达式的地方点加号输入任意表达式比如len(items)、data[user][name]、prices[-1]它会实时求值显示。这比一层层展开对象快得多也是我平时用得最多的功能。监视表达式还有一个隐藏价值它把我在关注什么这件事显式记录下来了切换栈帧时对照着看思路不容易乱。调试控制台是真正的大杀器。程序停下时控制台里可以执行任意 Python 表达式而且是运行在当前栈帧的上下文里。也就是说你可以直接敲items看变量敲len(items)算长度敲[x for x in items if x 0]过滤数据甚至调用函数。举几个我常用的# 看对象结构控制输出长度避免刷屏 import json; print(json.dumps(data, ensure_asciiFalse)[:800]) # 看 pandas 数据框的结构 df.shape df.dtypes # 临时改一个变量的值继续跑看后续影响 items items[:10]最后这条特别有用。有时候你想验证如果这个列表短一点后面的逻辑会不会出问题不需要改代码重跑整个流程停在断点上把变量一改按 F5 继续直接就在当前上下文里验证了。这种活体实验的能力是调试器相对日志的最大优势。3.3 异常断点与调用栈回溯程序崩溃报异常时默认情况下调试器会在异常抛出的位置停住吗答案是看配置。BREAKPOINTS 面板顶部有两个勾选项Raised Exceptions任何异常被抛出时立即停下包括那些被 try/except 正常捕获的Uncaught Exceptions只有异常最终没被捕获、导致程序崩溃时才停日常建议只勾 Uncaught Exceptions否则代码里正常的异常处理逻辑也会把你拦住非常烦人。只有当你在排查异常被吞掉了except 里逻辑不对这类问题时才临时勾上 Raised Exceptions这时你能看到异常抛出的第一现场而不是被 catch 之后丢失了上下文。异常停下之后调用栈CALL STACK面板就成了关键。它从上到下排列着从当前帧到最外层的调用链每一层都可以点击切换。切换过去之后变量面板会跟着显示那一层的局部变量这在排查参数在层层传递过程中被改坏了的问题时特别有效——从出错点往上翻一层层看变量在哪一步变了值问题的源头往往两三下就定位到了。我处理过一个数据管道的问题最终发现是某个中间层函数悄悄修改了传入的列表导致下游拿到了脏数据。就是靠调用栈一层层往上看变量的引用最后定位到那一行items.sort()它原地排序改掉了调用方的数据。这种问题如果只看日志得加多少 print 才能复原清楚。3.4 调试信息同时打印和落盘排查线上问题或者长时间运行的任务时光在终端看日志不够还需要把日志同时写进文件方便事后翻查。这个需求用 Python 的 logging 模块就能满足关键是配置两个 handlerimport logging def setup_logging(log_filedebug.log): fmt %(asctime)s [%(levelname)s] %(name)s:%(lineno)d - %(message)s handlers [ logging.StreamHandler(), # 输出到终端 logging.FileHandler(log_file, modea, encodingutf-8), # 写入文件 ] logging.basicConfig(levellogging.DEBUG, formatfmt, handlershandlers) if __name__ __main__: setup_logging() logger logging.getLogger(__name__) logger.debug(开始处理) logger.info(处理完成)两个 handler 各管一路StreamHandler负责终端实时可见FileHandler负责落盘留存。格式里带上%(lineno)d能直接定位到行号比只打消息好用得多。encodingutf-8必须显式指定否则在 Windows 上中文日志会乱码这个坑我踩过不止一次。另外 Python Debugger 还支持一个logToFile选项在 launch.json 的配置里加logToFile: true调试器自身的运行日志会写到.vscode/.logs目录下。这个日志记录的是调试适配器和被调试进程之间的通信一般用不上但当出现调试器连不上断点行为诡异这类玄学问题时它是唯一能看到底层细节的地方。还有一个容易忽略的点print和logging混用时输出顺序可能错乱。原因是二者用了不同的缓冲策略print默认行缓冲logging 走的是自己的 handler 链。统一用 logging 输出或者在调试配置里加上PYTHONUNBUFFERED: 1环境变量强制不缓冲顺序就正常了。4. 不同项目形态的调试落地方案前面讲的都是通用能力到了具体项目里脚本、Web 服务、远程部署、混合语言各有各的门道。这一章按形态分类给几套能直接用的配置。4.1 脚本与命令行参数跑带参数的脚本用args数组传递比在终端里拼命令然后接调试器简单得多{ name: 跑数据导入, type: debugpy, request: launch, program: ${workspaceFolder}/scripts/import_data.py, args: [--input, data/raw.csv, --limit, 1000, --verbose], cwd: ${workspaceFolder}, console: integratedTerminal }数组里每个元素就是一个独立参数--input和data/raw.csv要分成两个元素写别合成一个字符串。想让配置更灵活可以用${input:变量名}配合inputs字段在启动时弹输入框让你临时填参数值{ name: 带输入框的脚本, type: debugpy, request: launch, program: ${file}, args: [--date, ${input:runDate}], console: integratedTerminal }配套在 launch.json 顶层加inputs: [ { id: runDate, type: promptString, description: 请输入处理日期格式 YYYY-MM-DD, default: 2024-01-01 } ]这样每次启动会弹出框让你填日期适合那种每天跑一次、参数天天变的定时脚本。它把配置的固定部分和变化的参数分离开了不用每次改 json。4.2 Web 服务与接口调试 Flask 或 FastAPI 这类服务最大的坑是自动重载reloader。这些框架默认开启代码热重载会 fork 出一个子进程来跑真正的服务而调试器附加的是主进程结果就是断点明明打上了却永远不命中。解决办法是关掉 reloader把参数显式传给调试配置{ name: 调试 Flask, type: debugpy, request: launch, module: flask, env: { FLASK_APP: app.py, FLASK_DEBUG: 1 }, args: [run, --no-debugger, --no-reload], jinja: true, console: integratedTerminal }--no-reload是关键它禁止了子进程的生成让调试器能牢牢抓住主进程。FastAPI 用 uvicorn 启动的话同理把reloadFalse传进去或者用uvicorn.run(app, reloadFalse)。代价是改了代码要手动重启调试但换来断点稳定命中这笔买卖划算。另一个实用配置是jinja: true它让模板文件的断点也能工作。调后端接口返回的 HTML 有问题时能直接断在模板渲染那一行比猜哪里变量没传进去快得多。如果服务是以模块方式启动的用module字段替代program比如module: uvicorn配合args: [app.main:app, --port, 8000]效果等同于命令行里的uvicorn app.main:app --port 8000。4.3 远程服务器与 WSL代码跑在远程机器上本地只有编辑器这种情况用附加attach模式。远程机器上先装 debugpypip install debugpy然后以调试模式启动目标程序让它监听一个端口并等待客户端连接python -m debugpy --listen 0.0.0.0:5678 --wait-for-client app.py本地这边配上 attach 配置{ name: 附加到远程, type: debugpy, request: attach, connect: { host: 192.168.1.100, port: 5678 }, pathMappings: [ { localRoot: ${workspaceFolder}, remoteRoot: /opt/app } ] }pathMappings是这里最关键的字段。远程机器上的代码路径和本地的很可能不一样调试器需要知道本地这个目录对应远程那个目录才能在命中断点时正确地把源码定位到你的编辑器里。映射写错了断点会命中但打开的文件对不上或者干脆停在反汇编视图里。如果开发环境是 WSL那更简单装一个 WSL 扩展用在 WSL 中打开文件夹直接把项目打开之后的调试流程和本地一模一样不需要 attach也不需要 pathMappings因为编辑器本身就运行在 WSL 环境里路径是通的。这也是我目前最推荐的本地 Linux 开发方式。4.4 Python 调用 C 扩展的混合调试稍微进阶一点的情况Python 代码调用了 C 扩展或者 Cython 模块崩溃发生在 C 层纯 Python 调试器看不进去。这时候需要 gdb 出场通过设置让 Python 进程在启动时就进入可调试状态。思路是这样的先用一个 cppdbg 配置启动 Python 解释器本身把脚本作为参数传进去然后在 C 代码的符号上下断点。配置大致长这样{ name: 调试 C 扩展, type: cppdbg, request: launch, program: /usr/bin/python3, args: [${workspaceFolder}/test_ext.py], stopAtEntry: false, cwd: ${workspaceFolder}, MIMode: gdb, setupCommands: [ { description: 为 gdb 启用整齐打印, text: -enable-pretty-printing, ignoreFailures: true } ] }C 扩展编译时必须带-g参数保留调试符号否则 gdb 里只能看到地址看不到函数名等于白调。常用的 gdb 命令就那么几个break 函数名下断点run启动bt看调用栈frame N切帧print 变量看值next和step单步。这套组合拳在排查段错误、内存越界这类 Python 层面看不到的问题时是最后的倚仗。5. 常见问题与排查技巧实录前面讲的是怎么用这一章讲出错怎么办。我把这些年遇到的高频问题整理了一遍按症状分类配上原因和解决路径。5.1 断点是灰色空心圈永远不命中这是被问得最多的问题。灰色空心圈的含义是这个断点当前没有被调试器绑定到任何可执行的代码位置。常见原因有五种按出现频率排序第一文件没保存。调试器跑的是磁盘上的文件你的编辑还在缓冲区里。按一下CtrlS空心圈立刻变实心。这个原因简单到让人想笑但发生的频率高得惊人。第二解释器选错。调试器用的解释器和代码实际运行的环境不是同一个路径对不上自然绑不上。检查左下角状态栏显示的解释器和你的虚拟环境是否一致。第三justMyCode拦住了库代码。断点打在第三方库里被自动跳过了。改成false试试。第四多进程。程序 fork 了子进程断点打在子进程要执行的代码上但调试器默认只跟主进程。需要在配置里加subProcess: true让调试器跟踪子进程。第五代码是动态导入或者经过编译的。比如从数据库里读出来执行的字符串代码或者.pyc文件源码路径和调试器看到的不一致。排查顺序就按这个来从最简单的开始基本能覆盖绝大多数情况。5.2 ModuleNotFoundError 的几种来路ModuleNotFoundError: No module named xxx这个报错原因往往不在包没装而在调试器找错了环境。具体分几类包确实没装在当前选中的解释器里执行pip install xxx注意要在对应的虚拟环境里装虚拟环境选错状态栏显示的是全局 Python你却在虚拟环境里装了包两边对不上PYTHONPATH没配自己写的模块不在标准搜索路径里调试时因为 cwd 不同而找不到在 launch.json 里加env: {PYTHONPATH: ${workspaceFolder}}解决包名和导入名不一致比如安装的包叫Pillow导入时要写import PIL这种情况查一下包官方文档就行判断环境是否一致有个笨办法但很有效在调试控制台里执行import sys; print(sys.executable)它会打印出当前实际使用的解释器路径和你以为的那个对一下十有八九能看出问题。5.3 调试启动慢、卡死与子进程调试器启动慢通常有几个来源。justMyCode设成 false会让调试器分析大量第三方代码启动时间和单步速度都会明显变慢日常记得设回 true。监视表达式写得太重比如加了个sum([x.cost for x in huge_list])每次停下都要重算一遍程序越跑越卡。大对象自动展开VARIABLES 面板默认会尝试加载变量值遇到几百兆的数组时非常致命可以在设置里关掉自动加载改成手动触发。程序卡死但又不是死循环检查一下是不是用了internalConsole加input()的组合。前面提过内部调试控制台吃不下标准输入程序会安静地等在那里界面看起来就像卡死了。换成integratedTerminal立刻解决。调试子进程的问题则要区分场景。multiprocessing创建的子进程默认不被调试器跟踪要么用subProcess: true要么在子进程代码里手动加debugpy.wait_for_client()让它挂起等附加。5.4 问题速查表把上面的内容压缩成一张表出问题的时候照着对现象可能原因处理方式断点是灰色空心圈文件未保存 / 解释器不匹配 / justMyCode 拦截保存文件、核对解释器、调整 justMyCodeModuleNotFoundError环境选错 / PYTHONPATH 缺失检查 sys.executable、配置 env.PYTHONPATH断点命中但文件对不上路径映射缺失配置 pathMappings 的 localRoot 和 remoteRoot程序无故卡死internalConsole 不支持输入改用 integratedTerminalFlask 断点不命中热重载产生子进程启动参数加 --no-reload单步进库代码出不来justMyCode 为 false改回 true中文日志乱码FileHandler 未指定编码加 encodingutf-8多进程断点不生效未启用子进程调试配置 subProcess: true大对象展开导致卡顿变量面板自动求值关闭自动加载改用监视表达式输出顺序错乱print 与 logging 缓冲策略不同统一用 logging 或加 PYTHONUNBUFFERED5.5 几条压箱底的习惯工具会用了剩下的就是方法论。分享几个我这些年养成的习惯它们比任何配置都更影响调试效率。二分法定位比从头单步快得多。程序在两百行的地方出错别从第一行开始按 F11直接在中间打个断点看变量对不对不对就往回找对就往后找。十次以内的断点就能把问题框到几行代码的范围内。先复现再修复。调不出来就别急着改代码。写一个最小可复现脚本把无关的依赖全部剥掉往往在剥离的过程中问题自己就暴露了。这个习惯能省下大量改了三个地方不知道哪个起效了的困惑。日志断点优先于 print。临时加 print 爽清理起来烦还容易漏。日志断点不改代码、不留痕迹同一行能反复调整输出内容长期看效率高得多。调试和测试配合。单元测试帮你把问题缩小到单个函数调试器帮你进到函数内部看细节两者结合威力最大。我一般先用 pytest 跑出失败用例再在失败用例的代码路径上打断点定位速度比盲目断点快好几倍。打断点前先想清楚要看什么。这是最玄乎但最实在的一条。带着我要确认 items 在进入这个函数前是不是空列表这样的具体问题去打断点和漫无目的地在代码里撒红点效率差着数量级。调试器只是眼睛真正定位问题的是脑子里的假设。我个人的体会是真正让人从会用调试器变成调试高手的不是记住了多少配置参数而是学会了在按下 F5 之前先形成一个可以被证伪的假设。调试器的作用是快速验证这个假设假设对了就缩小范围假设错了就换个方向。这套思维一旦建立起来你会发现排查问题的速度是线性的、可预期的而不是靠运气瞎撞。