当 Python 程序出现异常时,直接添加 print() 往往只能看到零散的中间结果。pdb 是 Python 内置调试器,可以暂停程序、检查变量、逐行执行代码、设置条件断点,并结合调用栈定位问题。掌握这些基本操作后,调试过程会从“猜哪里错了”变成“观察程序正在做什么”。
从一个可运行的例子开始
下面的程序计算购物车总价,并根据折扣规则生成最终金额。示例故意保留一个容易观察的调试位置:折扣被应用在每件商品上,而业务规则可能要求对总价统一打折。
将代码保存为 debug_cart.py,然后运行它:
from dataclasses import dataclass
@dataclass
class Item:
name: str
price: float
quantity: int
def calculate_total(items: list[Item], discount: float) -> float:
total = 0.0
for item in items:
subtotal = item.price * item.quantity
total += subtotal
if discount > 0:
total -= subtotal * discount
return round(total, 2)
cart = [
Item("Keyboard", 299.0, 1),
Item("Mouse", 99.0, 2),
]
print(calculate_total(cart, 0.1))
使用 pdb 运行整个脚本:
python -m pdb debug_cart.py
进入调试器后,可以输入以下命令:
(Pdb) break calculate_total
(Pdb) continue
(Pdb) list
(Pdb) next
(Pdb) p items
(Pdb) p discount
(Pdb) p total
(Pdb) continue
这些命令分别用于设置函数断点、继续运行、查看当前代码、执行下一行,以及打印变量。p 是 print 的简写,适合快速检查表达式的值。
掌握四类高频操作
1. 单步执行并观察状态
常用的执行命令包括:
n或next:执行当前行,停在当前栈帧的下一行。s或step:执行当前行;如果遇到函数调用,会进入函数内部。r或return:继续运行,直到当前函数返回。c或continue:继续运行,直到遇到下一个断点或程序结束。l或list:显示当前代码附近的源代码。q或quit:退出调试器。
调试循环时,可以在每次 next 后检查 item、subtotal 和 total:
(Pdb) p item.name
'Keyboard'
(Pdb) p subtotal
299.0
(Pdb) p total
269.1
(Pdb) next
(Pdb) p item.name
'Mouse'
(Pdb) p subtotal
198.0
(Pdb) p total
447.3
这样可以清楚看到每一次循环如何改变累计值。调试器中的表达式也可以直接计算:
(Pdb) p item.price * item.quantity
(Pdb) p [item.name for item in items]
如果需要执行一段 Python 语句,可以使用 ! 前缀。例如,临时修改当前作用域中的变量:
(Pdb) !discount = 0
(Pdb) p discount
0
这类修改只影响当前调试进程,适合验证假设,不会改写源文件。
2. 使用断点缩小范围
除了按行号设置断点,也可以按函数名设置:
(Pdb) break calculate_total
如果已经停在某个位置,可以用 break 设置当前文件的行号断点:
(Pdb) break 16
断点太多时,可以查看和删除它们:
(Pdb) break
(Pdb) clear 1
调试大型循环时,普通断点可能会反复暂停。条件断点能让程序只在满足条件时停下。例如,只检查鼠标这一项:
(Pdb) break 14, item.name == "Mouse"
(Pdb) continue
也可以按数值条件暂停:
(Pdb) break 15, total < 0
条件表达式应当使用当前作用域中已经存在的变量,否则断点触发时可能产生额外错误或无法按预期工作。
用调用栈理解异常发生在哪里
当程序因为异常暂停,pdb 可以显示当前调用栈。输入 where 或 w:
(Pdb) where
调用栈会列出从外层调用到当前执行位置的路径。查看栈帧时,可以使用:
up:移动到调用当前函数的上一层栈帧。down:移动到下一层栈帧。args:显示当前函数的参数。p expression:查看当前栈帧中的变量或表达式。
例如,当一个服务函数调用 calculate_total() 时,where 能帮助你判断错误发生在请求处理层、业务函数,还是更深的辅助函数中。不要只盯着最后一行异常信息;调用栈同时提供了“错误在哪里抛出”和“程序是怎样走到这里的”两种信息。
在代码中嵌入断点
如果只想调试某个明确位置,可以在代码中加入 breakpoint():
def calculate_total(items: list[Item], discount: float) -> float:
total = 0.0
for item in items:
subtotal = item.price * item.quantity
breakpoint()
total += subtotal
if discount > 0:
total -= subtotal * discount
return round(total, 2)
然后正常运行:
python debug_cart.py
执行到 breakpoint() 时,Python 会进入调试器。这个方式适合临时检查局部逻辑;调试完成后应删除断点,或者在需要长期保留时显式使用更适合项目的调试配置。
一份实用的 pdb 检查清单
调试一个未知问题时,可以按下面的顺序操作:
- 在怀疑的函数入口设置断点:
break function_name。 - 用
continue运行到断点,确认传入参数:args、p variable。 - 用
list查看上下文,避免只看当前一行。 - 用
next逐步执行,用p检查关键状态变化。 - 对循环或请求数据设置条件断点,减少无关暂停。
- 遇到异常时执行
where,再用up和down检查调用链。 - 用
quit退出,并删除不再需要的代码断点。
pdb 的价值不在于命令数量多,而在于它让调试动作变得可重复:暂停一个明确位置,观察真实变量状态,验证一个具体假设,再决定下一步。对于简单脚本、数据处理程序和服务端业务函数,这套内置工具已经足以覆盖大量日常排错场景。