Python 怎么用 pdb 调试?breakpoint() 和事后调试怎么用?
简化版
pdb 是 Python 自带的交互式调试器,核心价值是「让程序在你想要的位置停下来,然后你可以查看任意变量、单步执行、甚至改变量继续跑」——比反复加 print 再重跑快一个数量级。打断点最简单的方式是 Python 3.7+ 的内置函数 breakpoint():在代码里写一行 breakpoint(),运行到这里就进入调试器;它比老写法 import pdb; pdb.set_trace() 更好的地方在于可以用环境变量 PYTHONBREAKPOINT=0 一键全局禁用(上线时不怕漏删),也可以 PYTHONBREAKPOINT=ipdb.set_trace 换成别的调试器。必须记住的命令(都有单字母缩写):n(next,执行下一行、不进函数)、s(step,进入函数)、c(continue,跑到下一个断点)、r(return,跑到当前函数返回)、l/ll(看代码)、p/pp(打印表达式)、w(where,看调用栈)、u/d(在栈帧间上下移动)、b(设断点,支持条件断点 b 42, x > 100)、q(退出)。最实用的两个高级用法:① 事后调试——程序崩溃后用 python -m pdb -c continue app.py 或 pdb.post_mortem() 直接进入异常发生时的现场,能查看当时所有局部变量;② pytest --pdb 让测试失败时自动落进调试器。核心记忆:breakpoint() 打断点,n/s/c/p/w 五个命令覆盖 90% 场景,崩溃现场用 post-mortem。
详细版
高频命令速查:
| 命令 | 全称 | 作用 |
|---|---|---|
n | next | 执行当前行,不进入函数内部 |
s | step | 执行当前行,进入函数内部 |
c | continue | 继续跑到下一个断点 |
r | return | 一直跑到当前函数返回 |
until N | until | 跑到行号 ≥ N(跳出循环的利器) |
l / ll | list / longlist | 看当前位置附近代码 / 看整个函数 |
p / pp | print / pretty print | 打印表达式 / 美化打印 |
w | where | 打印当前调用栈 |
u / d | up / down | 在调用栈里上移 / 下移一帧 |
a | args | 打印当前函数的所有参数 |
b 42 / b mod.func | break | 在行号/函数处设断点 |
b 42, x > 100 | —— | 条件断点(只在条件为真时停) |
cl / disable | clear / disable | 删除 / 停用断点 |
display x | display | 每次停下来自动显示 x 的值 |
interact | —— | 进入完整的 Python 交互式环境 |
q | quit | 退出调试器 |
# ① 最常用:代码里插一行(Python 3.7+)
def compute(items):
total = 0
for i, item in enumerate(items):
breakpoint() # ← 运行到这里进入调试器
total += item["value"]
return total
# 老写法(3.6 及以下):import pdb; pdb.set_trace()
# ★ breakpoint() 的独有好处:
# PYTHONBREAKPOINT=0 python app.py → 所有 breakpoint() 全部失效(上线保险)
# PYTHONBREAKPOINT=ipdb.set_trace python … → 换成 ipdb(带补全和语法高亮)
# ② 条件断点:只在"第 500 次循环"或"数据异常"时停
# (Pdb) b 12, i == 500
# (Pdb) b utils.py:88, item.get("value") is None
# ③ 事后调试(post-mortem):崩溃之后回到现场
# 方式 A(命令行,最省事):
# python -m pdb -c continue app.py
# → 正常跑,一旦抛出未捕获异常,自动停在★出错的那一帧★
# 方式 B(代码里):
import pdb, sys, traceback
try:
risky()
except Exception:
traceback.print_exc()
pdb.post_mortem(sys.exc_info()[2]) # 进入异常现场
# 方式 C(交互式解释器里刚崩完):
# >>> import pdb; pdb.pm()
# ④ 测试里调试
# pytest --pdb # 失败时自动进 pdb
# pytest --trace # 每个测试开头就进 pdb
# pytest -x --pdb -k test_parse # 只跑某个用例、第一次失败就停
# ⑤ 调试时可以改变量、执行任意代码(不只是看)
# (Pdb) p item
# (Pdb) item["value"] = 0 # ★直接改,然后 c 继续跑★
# (Pdb) pp {k: type(v) for k, v in item.items()}
# (Pdb) interact # 进入完整 Python 交互环境(Ctrl-D 退回 pdb)
⚠️ 三个最容易卡住新手的点:①
n和s的区别——n(next)把函数调用当成「一行」整体执行完,s(step)会钻进被调用的函数里。经验法则是默认按n,只有确认问题在某个函数内部时才s;不小心s进了标准库源码就按r(跑到该函数返回)或u(回到上一帧)出来。② 变量名和 pdb 命令冲突:如果你的局部变量叫n、c、l、s、p、b,直接输入名字会被当成命令执行(n会单步、c会继续跑)。解决办法是用p n显式打印,或用!n(感叹号表示「这是一条 Python 语句」,如!n = 5给变量赋值)。③c之后不会再停(除非还有别的断点),所以在循环里调试时,与其反复c,不如下条件断点b 12, i == 500或用until直接跳出循环——这是把「调试一小时」变成「调试一分钟」的关键技巧。
完整版教学
一、为什么值得学调试器:print 大法的三个成本
print 调试的真实代价(以"某次循环里数据异常"为例):
① 每加一处 print 就要★重跑一遍★
程序启动 20 秒 + 跑到出错处 3 分钟 → 每验证一个猜测要 3.5 分钟
试 10 个猜测 = 35 分钟;而 pdb 一次断点就能把所有变量都看一遍
② 你必须★提前猜对★要打印什么
print(item) 之后发现该看的是 item 的类型/长度/上层变量 → 再改再跑
pdb 里可以现场 p 任何表达式、pp 任何嵌套结构、w 看整条调用栈
③ 清理成本 + 泄漏风险
调完要删干净;漏删的 print 会污染日志,甚至打印出敏感数据
调试器的独有能力(print 做不到的):
- 看★调用栈的任意一层★的变量(w 看栈、u/d 上下移动)
- ★修改变量后继续执行★(验证"如果这里是 0 会怎样",不用改代码重跑)
- ★条件断点★(第 50 万次循环、或某字段为 None 时才停)
- ★事后调试★(崩溃后回到异常现场,看当时所有局部变量)
- 执行任意代码(interact 进入完整 Python 环境)
什么时候 print/logging 仍然更好:
- 多线程/异步的时序问题(断点会改变时序,"一停就不复现")
- 生产环境(不可能在线上开断点)→ 用结构化日志
- 需要"整体趋势"而不是"某一时刻"(每次循环的值分布)
→ 结论:logging 用于"长期可观测",pdb 用于"当下这个 bug"
调试器和 print 不是对立关系,但大多数人低估了 print 大法的成本:每验证一个猜测都要改代码 + 重跑一遍,如果程序启动加复现要三分钟,试十个猜测就是半小时;而且你必须提前猜对该打印什么——打印了 item 才发现该看的是它的类型或者上层调用者传了什么。调试器把这个循环压缩成一次:断点停下后,所有局部变量、整条调用栈、任意表达式都可以随时查看,还能改掉变量继续跑来验证假设(「如果这个字段是 0 会怎样」不用改代码重跑)。反过来也要承认它的边界:多线程/异步的时序 bug 一加断点就不复现,生产环境更不可能开断点——这些场景属于 logging 和 py-spy 的地盘。一句话分工:logging 负责长期可观测性,pdb 负责当下这个 bug。
二、怎么进入调试器:四种入口
入口 1:breakpoint()(★Python 3.7+ 首选★)
def f(x):
breakpoint() # 运行到此进入 pdb
return x * 2
相比老写法 import pdb; pdb.set_trace() 的三个优势:
① 短、内置,不用 import
② PYTHONBREAKPOINT=0 可以★一键全局禁用★
→ 上线前不小心漏删也不会卡住服务(这是它最大的价值)
③ PYTHONBREAKPOINT=ipdb.set_trace 可以★换成任意调试器★
(ipdb 有语法高亮和 Tab 补全;pudb 是全屏 TUI)
★ 它的实现就是调用 sys.breakpointhook(),默认指向 pdb.set_trace
入口 2:命令行启动(从第一行就开始调试)
python -m pdb app.py # 停在第一行,然后 b 设断点、c 开跑
python -m pdb -c "b 42" -c c app.py # ★-c 预先执行 pdb 命令★(自动化)
入口 3:事后调试(程序已经崩了)
python -m pdb -c continue app.py # 直接跑,崩了就停在异常帧 ← ★最常用★
代码里:pdb.post_mortem(tb) 或 交互式里刚崩完:pdb.pm()
入口 4:测试框架集成
pytest --pdb # 断言失败/异常时进入 pdb
pytest --trace # 每个用例开头就停
pytest -x --pdb -k test_name # 只跑一个用例、失败立刻停
★ 远程/容器里怎么办(本地 pdb 用不了):
- 用 remote-pdb / debugpy(VSCode 可 attach)
- 只想看"卡在哪"而不是断点:py-spy dump --pid 1234(★零侵入、不停服务★)
- 容器里 docker exec -it 后再 attach(需要进程有 tty)
breakpoint() 是 Python 3.7 引入的内置函数,取代了 import pdb; pdb.set_trace() 这个流传多年的咒语。除了短,它真正的价值是那两个环境变量开关:PYTHONBREAKPOINT=0 能一键让所有 breakpoint() 失效(上线时哪怕漏删一行也不会把服务卡死在调试器里,这是老写法做不到的安全网),而 PYTHONBREAKPOINT=ipdb.set_trace 能把默认调试器换成带补全和高亮的 ipdb——原理是它会调用可替换的 sys.breakpointhook()。另外三个入口各有场合:python -m pdb app.py 从第一行开始调试(配合 -c 预执行命令可以自动化设断点);python -m pdb -c continue app.py 是排查崩溃的最高效姿势——正常跑,一旦抛出未捕获异常就停在出错的那一帧;pytest --pdb 让失败的测试直接把你送进现场。至于容器和线上,本地调试器往往用不了,此时 py-spy dump --pid 能零侵入地打印所有线程的当前调用栈,是排查「卡住了」的首选。
三、命令体系:五个命令覆盖 90% 场景
移动类(决定"下一步执行到哪"):
n (next) 执行当前行;遇到函数调用★整体执行完★,不进去
s (step) 执行当前行;遇到函数调用★钻进去★
r (return) 一直执行到当前函数 return(用来"从误入的函数里出来")
c (continue) 继续跑,直到下一个断点或程序结束
until 120 一直跑到行号 ≥ 120(★跳出循环的利器★,不用按 100 次 n)
j 42 (jump) ★直接跳到第 42 行执行★(可回退重试,但不能跳进/跳出循环体等)
查看类:
l (list) 显示当前行前后 11 行;再按 l 继续往下翻
ll (longlist) 显示★整个当前函数★的源码(比 l 好用)
w (where) 打印调用栈(当前帧用 > 标出)
a (args) 打印当前函数的所有参数值
p 表达式 打印(任意 Python 表达式,如 p len(items)、p obj.__dict__)
pp 表达式 pretty print(嵌套 dict/list 会格式化换行,★看大结构必用★)
display x 每次停下自动显示 x(值变了才提示);undisplay 取消
栈帧类(★调试的关键能力★):
u (up) 上移一帧(去到"调用我的那个函数",看它的局部变量)
d (down) 下移一帧
→ 异常现场里最有用:崩在第三方库里,u 几次回到自己的代码看是哪个参数不对
断点类:
b 列出所有断点
b 42 在当前文件第 42 行设断点
b utils.py:88 在指定文件的行设断点
b mymod.parse 在函数入口设断点
b 42, x > 100 ★条件断点★:只有 x > 100 时才停
tbreak 42 临时断点(命中一次后自动删除)
cl 1 删除 1 号断点;cl 删除全部(会问 y/n)
disable 1 / enable 1 停用/启用(保留断点定义)
ignore 1 500 ★忽略前 500 次命中★(等价于"第 501 次才停")
执行任意代码:
!语句 执行 Python 语句(如 !x = 5 改变量、!import json)
interact 进入完整 Python 交互环境(Ctrl-D 退回 pdb)
★ 变量名和命令冲突时:变量叫 n/c/l/s/p/b/a 时直接输名字会执行命令
→ 用 p n 打印,用 !n = 5 赋值
★ 直接回车 = 重复上一条命令(连续按 n 时非常顺手)
命令看着多,实际五个就覆盖 90% 的场景:n、s、c、p、w。要真正提速,关键是掌握三组「省时间」的命令:① until 和 ignore——在循环里调试时,与其按一百次 n 或反复 c,不如 until 120 直接跳出循环、或 ignore 1 500 让断点忽略前 500 次命中;② u/d 在栈帧间移动——异常往往崩在第三方库深处,w 看栈后 u 几次回到自己的代码,才能看到「是我传错了什么参数」;③ pp 和 display——pp 把嵌套的 dict/list 格式化输出,display x 让每次停下自动显示某个变量的变化。两个必须记住的细节:变量名和命令冲突时用 p 名字 查看、!名字 = 值 赋值(! 表示「这是一条 Python 语句」);直接按回车会重复上一条命令,连续单步时非常顺手。
四、断点技巧:把「调一小时」变成「调一分钟」
场景 1:循环跑 100 万次,只有某一次数据异常
✗ 笨办法:breakpoint() 放循环里 → 按 c 按到手废
✓ 条件断点:
(Pdb) b 42, item.get("value") is None
(Pdb) c # 直接跳到出问题的那一次
✓ 或者代码里写条件:
if item.get("value") is None:
breakpoint()
场景 2:知道最终会崩,但不知道在哪一层
✓ python -m pdb -c continue app.py
→ 崩溃时自动停在异常帧,w 看栈、u 回到自己的代码、p 查看当时的变量
★ 这比看 traceback 强的地方:traceback 只有行号,pdb 能看到★当时所有变量的值★
场景 3:第三方库里报错,想看它拿到了什么参数
✓ b requests/models.py:812 # 直接给库文件下断点
✓ 或崩溃后 post-mortem,然后 u 逐层上移,a 打印每层的参数
场景 4:想验证"如果这个值不同会怎样"
✓ (Pdb) !config["timeout"] = 30
(Pdb) c # 改完继续跑,不用改代码重启
场景 5:只想跳过某段代码(跳过一次昂贵的初始化)
✓ (Pdb) j 88 # jump 到第 88 行
★ 限制:不能跳进/跳出 for 循环体、finally 块、以及跨函数
场景 6:想每次停下都看某几个变量
✓ (Pdb) display total
(Pdb) display len(buf)
→ 之后每次停下会自动显示(且只在★值变化时★提示)
进阶:用 .pdbrc 保存常用配置(放家目录或项目根目录)
# ~/.pdbrc
alias pi pp {k: v for k, v in %1.__dict__.items() if not k.startswith('_')}
alias ll longlist
→ 每次进 pdb 自动加载,把高频操作固化成一条命令
真正拉开调试效率差距的不是命令数量,而是会不会用条件断点。「循环跑一百万次、只有某一次出问题」是最典型的场景:按 c 按到手废是新手做法,写 b 42, item.get("value") is None 一次到位才是熟手做法(等价的代码写法是把 breakpoint() 放进 if 异常条件: 里,在需要复杂判断时更灵活)。第二个高价值技巧是事后调试 + 栈帧移动:python -m pdb -c continue app.py 让程序正常跑、崩了自动停在异常帧,此时 w 看栈、u 逐层上移、a 打印每层参数——这比看 traceback 强的地方在于 traceback 只有行号和异常信息,而 pdb 能看到崩溃瞬间所有变量的实际值。再加上 !变量 = 新值 现场改数据继续跑(验证假设不用改代码重启)、j 跳过昂贵的初始化、display 自动跟踪变量变化,以及把高频操作写进 .pdbrc,一小时的排查常常能压到几分钟。
五、事后调试:崩溃现场是最有价值的信息
traceback 给你的:出错的文件、行号、异常类型和消息
post-mortem 给你的:★出错瞬间每一层栈帧的全部局部变量★
三种进入方式:
① 命令行(推荐,零代码改动):
python -m pdb -c continue app.py
Traceback ... KeyError: 'user_id'
> app.py(42)parse()
(Pdb) p record # ← 看到底是哪条记录没有 user_id
(Pdb) w # ← 看是谁传进来的
(Pdb) u # ← 上移到调用方,看它的变量
② 代码里包住可疑区域:
import pdb, sys, traceback
try:
risky()
except Exception:
traceback.print_exc()
pdb.post_mortem(sys.exc_info()[2])
③ 交互式解释器里刚崩完:
>>> run_something()
Traceback ...
>>> import pdb; pdb.pm() # pm = post mortem,回到刚才的异常现场
④ 全局兜底(脚本调试期用,★别上生产★):
import sys, pdb
def hook(exc_type, exc, tb):
traceback.print_exception(exc_type, exc, tb)
pdb.post_mortem(tb)
sys.excepthook = hook
在异常现场最该做的四件事:
1. w 看完整调用栈(搞清是哪条路径过来的)
2. u/d 移动到★自己写的那一帧★(异常常发生在库内部)
3. a / p 打印参数和关键变量(找到"哪个值不对")
4. pp obj.__dict__ / p type(x) / p len(x) 看清对象的真实形态
★ 一个关键限制:post-mortem 需要异常★没有被捕获★(或你手动传 tb 进去)
被 except 吞掉的异常不会触发 post-mortem
→ 调试期可以临时把 except 改成 except Exception: raise,或在 except 里 post_mortem
pytest 里的等价物:
pytest --pdb # 失败即进入现场(★测试调试最常用★)
pytest --pdbcls=IPython.terminal.debugger:TerminalPdb --pdb # 换成 ipdb
事后调试(post-mortem)是 pdb 最被低估的功能。traceback 只告诉你「在 app.py:42 抛了 KeyError: 'user_id'」,而 post-mortem 让你回到崩溃发生的那一瞬间,看到每一层栈帧里所有变量的真实值——「到底是哪条记录缺字段」「上游传进来的参数长什么样」这些问题当场就有答案,不需要加日志重跑。最省事的用法是命令行 python -m pdb -c continue app.py(零代码改动),交互式环境里刚崩完则可以 import pdb; pdb.pm()。进入现场后的标准动作是四步:w 看完整调用栈 → u/d 移动到自己写的那一帧(异常经常发生在第三方库内部)→ a/p 打印参数和关键变量 → pp obj.__dict__ 看清对象真实形态。一个关键限制要记住:post-mortem 依赖异常「没有被捕获」,被 except 吞掉的异常不会触发它——调试期可以临时在 except 里手动调 pdb.post_mortem()。
六、pdb 的局限与更好的选择
pdb 不擅长的场景:
① 多线程 / 异步
- 断点只停当前线程,其他线程继续跑 → 状态在你眼皮底下变化
- 断点会改变时序 → 竞态 bug "一调试就不复现"(海森堡 bug)
→ 用日志 + 线程名,或 py-spy dump 看所有线程的栈
② 生产环境
- 不能停服务;也不该在生产暴露交互式 shell(安全风险)
→ 结构化日志 + py-spy(采样、attach、零侵入)+ APM
③ 长循环里"看趋势"
- 想知道 100 万次循环里值的分布 → 断点没意义
→ 打日志/收集统计再分析
④ 界面朴素(无高亮、无补全、看不到上下文)
替代与增强:
┌────────────────┬──────────────────────────────────────┐
│ ipdb │ pdb + Tab 补全 + 语法高亮(最低成本升级)│
│ pudb │ 全屏 TUI,同时显示源码/变量/栈,很直观 │
│ IDE 调试器 │ VSCode/PyCharm:鼠标点断点、悬停看值 │
│ debugpy │ 远程/容器调试(VSCode attach 到进程) │
│ py-spy dump │ ★不停进程★看所有线程当前栈(线上首选) │
│ faulthandler │ 标准库:段错误/卡死时自动打印栈 │
│ logging │ 长期可观测性(生产唯一可行方案) │
└────────────────┴──────────────────────────────────────┘
用 ipdb 替换默认调试器(一行环境变量,不改代码):
PYTHONBREAKPOINT=ipdb.set_trace python app.py
上线保险(★重要★):
- CI 里加检查:禁止提交 breakpoint() / pdb.set_trace()
(ruff 规则 T100、flake8-debugger 都能检测)
- 运行时兜底:生产环境设 PYTHONBREAKPOINT=0
→ 双保险,避免"漏删一行断点把服务卡死"这种事故
选择心法:
当下这个 bug、能本地复现 → pdb / IDE 调试器
线上、不能停 → 日志 + py-spy
时序/并发 → 日志(断点会改变时序)
性能问题 → profiler(不是调试器)
pdb 有明确的边界,硬用会越调越乱。多线程和异步是第一个雷区:断点只停当前线程、其他线程继续跑,而且断点本身改变了时序,导致竞态 bug「一调试就不复现」(所谓海森堡 bug)——这类问题只能靠日志和 py-spy dump。生产环境是第二个雷区:既不能停服务,也不该暴露交互式 shell(能执行任意代码,是严重的安全风险),线上定位靠结构化日志加 py-spy(采样、可 attach、零侵入)。工具上有几个低成本升级值得知道:ipdb 给 pdb 加上 Tab 补全和语法高亮(用 PYTHONBREAKPOINT=ipdb.set_trace 一个环境变量就能换,不改代码)、pudb 是全屏 TUI、IDE 调试器适合复杂断点场景、faulthandler(标准库)能在段错误或卡死时自动打印栈。最后是工程纪律:CI 里用 ruff 的 T100 规则禁止提交 breakpoint(),生产环境同时设 PYTHONBREAKPOINT=0 做兜底,双保险避免漏删的断点把服务卡死。
记忆钩子:「调试的入口只需记一个:
breakpoint()(3.7+ 内置,取代import pdb; pdb.set_trace()),它的独门价值是环境变量开关——PYTHONBREAKPOINT=0一键全局禁用(上线漏删也不怕)、PYTHONBREAKPOINT=ipdb.set_trace一键换成带补全高亮的 ipdb。★命令五个够用:n(下一行,不进函数)、s(进函数)、c(跑到下个断点)、p/pp(打印表达式)、w(看调用栈)+ u/d(在栈帧间上下移动,异常崩在库里时靠它回到自己的代码)。★真正提速的是三招:条件断点b 42, x is None(循环百万次只停出问题那次)、until/ignore 1 500(跳出循环、忽略前 N 次命中)、!x = 5现场改变量继续跑(验证假设不用改代码重启)。★最被低估的是事后调试:python -m pdb -c continue app.py让程序正常跑、崩了自动停在异常帧——traceback 只有行号,post-mortem 能看到崩溃瞬间每一层栈帧的全部变量值(交互式里刚崩完用pdb.pm(),测试里用pytest --pdb)。★两个卡人的小坑:变量名和命令冲突时要用p n打印、!n = 5赋值;直接回车=重复上一条命令。★边界:多线程/异步的时序 bug 一加断点就不复现、生产环境绝不能开(用日志 + py-spy dump),CI 里用 ruff T100 禁止提交断点、生产设 PYTHONBREAKPOINT=0 兜底。」
七、常见误区与追问
- 误区:
breakpoint()只是import pdb; pdb.set_trace()的简写,用哪个都一样。 短只是表象,真正的区别在于breakpoint()走的是可替换的sys.breakpointhook(),因此获得两个老写法没有的能力:①PYTHONBREAKPOINT=0能一键让所有breakpoint()失效——这是重要的生产安全网,万一有人漏删一行断点提交上线,服务也不会卡死在调试器里等一个永远不会来的输入;②PYTHONBREAKPOINT=ipdb.set_trace(或 pudb、debugpy)能整体切换调试器,团队里谁想用带补全高亮的 ipdb,设个环境变量就行,不用改一行代码。而pdb.set_trace()是硬编码的,两种能力都没有。 - 误区:
n和s差不多,随便按哪个都能往下走。 差别很关键:n(next)把函数调用当作「一行」整体执行完,不进入函数体;s(step)会钻进被调用的函数内部。不清楚这个区别最常见的后果是手一抖s进了标准库或第三方库的源码,然后在几十层陌生代码里迷路。经验法则是默认按n,只有确认问题出在某个函数内部时才s;不小心进去了,用r(跑到当前函数返回) 或u(上移一帧回到调用方) 出来。另外until N能一次跑到指定行(跳出循环神器),c则一直跑到下一个断点——熟练组合这几个命令,比一路按n快得多。 - 误区:程序崩了只能看 traceback,然后加 print 重跑。 这正是事后调试(post-mortem) 要解决的问题:traceback 只有文件、行号、异常类型,而 post-mortem 让你回到崩溃发生的那一瞬间,查看每一层栈帧的全部局部变量。最省事的方式是
python -m pdb -c continue app.py——程序正常跑,一旦抛出未捕获异常就自动停在异常帧,然后w看栈、u回到自己的代码、p查看当时的值;交互式解释器里刚崩完可以import pdb; pdb.pm();测试里用pytest --pdb。唯一的前提是异常没有被except吞掉(被捕获的异常不会触发 post-mortem,需要在except里手动调pdb.post_mortem())。 - 误区:在循环里下个断点,然后一直按
c就能调到出问题的那次。 循环一百万次时这是不可行的。正确做法有三种:① 条件断点b 42, item.get("value") is None——只在条件为真时才停,一次到位;②ignore 1 500——让 1 号断点忽略前 500 次命中(等价于「第 501 次才停」),适合已知「第 N 次出问题」的情况;③ 代码里写if 异常条件: breakpoint(),在判断逻辑复杂时比条件断点表达式更灵活。此外until 行号能直接跑到循环之后的某一行,省去按几百次n。会不会用这几招,是调试效率相差一个数量级的分水岭。 - 误区:变量叫
n或c时,在 pdb 里输入名字就能看到它的值。 不能——pdb 会优先把输入当作命令,输入n会执行「单步」、输入c会「继续运行」,你的程序就这么被推着往前跑了,还找不到原因。解决办法有两个:查看用p n(p后面跟任意 Python 表达式),赋值和执行语句用!前缀(!n = 5、!import json),感叹号明确告诉 pdb「这是一条 Python 语句而不是命令」。相关的还有一个常用小技巧:直接按回车会重复上一条命令,连续单步时很顺手,但也意味着你如果刚按过c,随手一个回车就又继续跑了。 - 追问:为什么多线程和异步程序不适合用 pdb 调试? 两个原因。① 断点只停住当前线程,其他线程继续运行——你在断点处看到的「状态」可能在你敲下一条命令的过程中已经被别的线程改掉了,看到的是幻觉;协程也类似,事件循环里其他任务的行为会被打乱。② 断点本身改变了时序:竞态条件(race condition)依赖特定的执行交错,一旦某个线程被停住几秒钟,那个交错就不会出现了——bug「一调试就不复现」,这就是所谓的海森堡 bug(观测行为改变了被观测对象)。这类问题的正确工具是带线程名和时间戳的结构化日志(不改变时序)、
py-spy dump --pid(零侵入地打印所有线程当前栈,排查死锁和卡死)、以及针对性的压力测试和threading的同步原语审查。 - 追问:线上服务出问题,不能用 pdb,那用什么? 分三种情况。① 卡住/不响应:
py-spy dump --pid <PID>打印所有线程的当前调用栈——不需要改代码、不需要重启、开销极小,一眼看出卡在哪个函数(等锁、等网络、还是死循环);标准库的faulthandler(faulthandler.enable()或-X faulthandler)也能在收到特定信号或段错误时 dump 栈。② 慢:py-spy top --pid实时看 CPU 花在哪些函数上,或py-spy record出火焰图。③ 逻辑错误:只能靠结构化日志(带 request_id、关键参数、耗时)和 APM 链路追踪事后还原——这也是为什么日志要在平时就打好,出事时没有日志就只能盲猜。绝对不要在生产暴露交互式调试器:它能执行任意代码,是严重的安全风险,且会挂起服务。 - 追问:怎么防止
breakpoint()被误提交到生产? 双保险。第一层是 CI 静态检查:ruff的T100规则(对应flake8-debugger插件)能检测出代码里的breakpoint()、pdb.set_trace()、ipdb.set_trace()等调试语句,在 lint 阶段直接让流水线失败;也可以配 pre-commit hook 在本地提交时就拦住。第二层是运行时兜底:生产环境统一设置环境变量PYTHONBREAKPOINT=0,这样即使真有一行漏网的breakpoint()上了线,它也只是空操作(sys.breakpointhook被设成什么都不做),服务不会挂起等待一个永远不会到来的输入。这两层配合,才能既放心地在开发时随手插断点,又不担心它跑到线上——这也正是breakpoint()相比pdb.set_trace()的核心优势。
八、加强记忆
pdb 的价值是把「改代码加 print → 重跑 → 再猜」的循环压缩成一次断点:停下后所有局部变量、整条调用栈、任意表达式都能随时查看,还能改掉变量继续跑来验证假设。入口只需记 breakpoint()(Python 3.7+ 内置,取代 import pdb; pdb.set_trace()),它的独门价值来自可替换的 sys.breakpointhook()——PYTHONBREAKPOINT=0 一键全局禁用(上线漏删也不会卡死服务)、PYTHONBREAKPOINT=ipdb.set_trace 一键换调试器。命令五个够用:n(下一行、不进函数)、s(进函数,误入库源码就用 r 或 u 出来)、c(跑到下个断点)、p/pp(打印/美化打印表达式)、w(看调用栈),再加 u/d 在栈帧间上下移动(异常崩在第三方库里时,靠它回到自己的代码看是哪个参数不对)。真正拉开效率的是三招:条件断点 b 42, x is None(循环百万次只停在出问题那次)、until 行号 / ignore 1 500(跳出循环、忽略前 N 次命中)、!x = 5 现场改变量继续跑(验证假设不用改代码重启)。最被低估的是事后调试:python -m pdb -c continue app.py 让程序正常跑、崩了自动停在异常帧——traceback 只有行号,post-mortem 能看到崩溃瞬间每一层栈帧的全部变量值(交互式里刚崩完用 pdb.pm(),测试里用 pytest --pdb);前提是异常没有被 except 吞掉。两个卡人的小坑:变量名与命令冲突时用 p n 查看、!n = 5 赋值;直接回车等于重复上一条命令。边界要清楚:多线程/异步的时序 bug 一加断点就不复现(断点只停当前线程且改变时序),生产环境绝不能开交互式调试器——线上排查用结构化日志 + py-spy dump/top;工程上用 ruff 的 T100 规则拦住提交、生产设 PYTHONBREAKPOINT=0 兜底。