Python 系列(六十二) 使用 pdb 逐步调试程序,定位代码问题
编写 Python 程序时,我们经常会遇到这样的情况:程序可以运行,但是输出结果不符合预期;或者程序运行到一半就报错了,我们却不知道问题出在哪里。
例如,我们希望计算服务器的 CPU 使用率,却发现最终结果不对。检查代码时,每一行看起来都没有明显的问题,该怎么办?
如果只能通过添加 print() 语句来查看变量的值,不仅需要反复修改代码,还可能需要添加很多调试信息。
Python 提供了一个内置的调试工具:pdb。它属于 Python 标准库,不需要额外安装,可以帮助我们暂停程序、逐步执行代码、查看变量的值,从而找到程序中的问题。
这篇文章将从零开始,学习如何使用 pdb 调试 Python 程序。
认识 pdb
pdb 是 Python 提供的交互式调试器,可以让我们在程序运行过程中暂停执行,并通过输入调试命令观察程序的运行状态。
可以先把调试理解成:让程序暂时停下来,一步一步检查它到底在做什么。
例如,假设我们正在编写一个统计服务器 CPU 使用率的程序:
1 | cpu_total = 100 |
运行程序:
1 | python debug_demo.py |
输出结果:
1 | CPU 使用率:80% |
这个程序可以正常运行,但如果我们发现结果不符合预期,就需要检查变量的值,以及计算过程是否正确。
我们可以使用 pdb,让程序在执行到指定位置时暂停下来。
创建第一个调试程序
先创建一个 Python 文件,文件名为 debug_demo.py。
在文件中输入以下代码:
1 | import pdb |
逐行认识这段代码。
import pdb:导入 Python 标准库中的调试模块。cpu_total = 100:定义一个变量,表示 CPU 总量。cpu_idle = 20:定义一个变量,表示空闲 CPU 对应的数值。pdb.set_trace():设置一个调试断点,让程序进入调试交互界面。cpu_used = cpu_total - cpu_idle:计算已使用的 CPU 数值。print(...):输出计算结果。
这里最重要的是这一行:
1 | pdb.set_trace() |
它相当于在程序中放置了一个暂停点。
执行到 pdb.set_trace() 时,这条语句本身已经执行完毕,程序会暂停在后续代码执行之前。
也就是说,程序暂停时:
cpu_total = 100已经执行,变量cpu_total的值为100。cpu_idle = 20已经执行,变量cpu_idle的值为20。cpu_used = cpu_total - cpu_idle尚未执行,因此这个变量还没有通过该语句完成赋值。print(...)尚未执行,因此屏幕上还没有输出 CPU 使用率。
理解这个执行顺序非常重要,因为调试时能否查看一个变量,取决于程序是否已经执行了创建或赋值该变量的代码。
启动 pdb 调试
打开终端或命令提示符,进入 debug_demo.py 所在的目录,然后执行:
1 | python debug_demo.py |
如果你的环境使用 python3 命令,也可以执行:
1 | python3 debug_demo.py |
程序会暂停,并显示类似下面的内容:
1 | > debug_demo.py(7)<module>() |
这里假设 pdb.set_trace() 位于第 6 行,具体行号可能因文件内容不同而变化。
这段信息包含几个重要内容:
debug_demo.py表示当前调试的文件。(7)表示调试器当前显示的代码位置是文件中的第 7 行。-> cpu_used = cpu_total - cpu_idle表示接下来准备执行的代码语句。(Pdb)是调试器的命令提示符,表示程序已经暂停,正在等待我们输入调试命令。
因此,虽然程序是通过 pdb.set_trace() 进入调试器的,但当前显示的待执行语句通常是断点后面的下一条语句,而不是 pdb.set_trace() 本身。
断点后的完整交互过程
为了真正掌握 pdb,我们先完整地操作一遍,看看程序暂停后应该输入什么命令,以及每一步会发生什么。
当前调试的程序是:
1 | import pdb |
运行程序后,调试器暂停在 cpu_used 的赋值语句之前。
查看已经创建的变量
在 (Pdb) 后输入:
1 | p cpu_total |
输出:
1 | 100 |
继续输入:
1 | p cpu_idle |
输出:
1 | 20 |
这说明程序执行到断点时,这两个变量已经存在,并且分别保存着 100 和 20。
现在尝试查看尚未赋值的变量:
1 | p cpu_used |
由于赋值语句还没有执行,调试器会提示类似下面的错误:
1 | *** NameError: name 'cpu_used' is not defined |
这不是原来的程序发生了新的错误,而是我们在调试过程中访问了一个尚未定义的变量。
执行下一条语句
现在输入:
1 | n |
调试器会执行当前显示的赋值语句:
1 | cpu_used = cpu_total - cpu_idle |
然后暂停在下一条语句执行之前,显示类似下面的内容:
1 | > debug_demo.py(9)<module>() |
这里的行号仅为示例。
此时,cpu_used 已经完成赋值。
输入:
1 | p cpu_used |
输出:
1 | 80 |
再输入:
1 | p cpu_used / cpu_total * 100 |
输出:
1 | 80.0 |
这说明我们不仅可以查看变量,还可以在调试器中直接计算表达式,检查程序中的数据是否符合预期。
执行输出语句
现在,调试器停在 print() 语句之前。
输入:
1 | n |
程序会执行输出语句,显示:
1 | CPU 使用率:80% |
由于当前程序已经执行完毕,调试会话随之结束,返回普通终端。
完整终端交互记录
把上面的步骤连起来,整个调试过程大致如下:
1 | C:\python-demo>python debug_demo.py |
以上是示意交互记录。实际显示的路径、行号和提示信息可能因操作系统、Python 版本及代码内容而有所不同。
通过这个过程,我们可以看到 pdb 的基本工作方式:
- 程序运行到
pdb.set_trace(),进入调试器。 - 使用
p检查已经存在的变量。 - 使用
n执行当前待执行语句。 - 再次使用
p检查执行后的变量状态。 - 继续使用
n执行剩余语句,直到程序结束。
需要注意,程序正常结束后,调试器通常会自动退出,因此不需要再输入 q。
查看变量的值
前面的完整交互已经介绍了 p 命令。这里再单独总结一下它的用途。
p 是 print 的简写形式,可以在调试过程中输出变量或表达式的结果。
例如:
1 | (Pdb) p cpu_total |
前两条命令分别查看变量的值,第三条命令直接计算两个变量相减的结果。
如果程序输出不符合预期,可以先检查参与计算的变量是否正确,再检查计算表达式本身。
需要注意,变量必须已经在当前作用域中定义,才能直接通过变量名查看其值。如果赋值语句还没有执行,访问该变量就可能出现 NameError。
逐步执行程序
除了查看变量,pdb 还允许我们控制程序的执行过程。
下面介绍几个常用的调试命令。
| 命令 | 作用 |
|---|---|
n | 执行当前待执行语句,并停在后续代码位置 |
s | 执行当前语句;遇到函数调用时,可以进入函数内部 |
c | 继续运行程序,直到遇到下一个断点或程序结束 |
l | 查看当前代码附近的内容 |
p 变量名 | 查看变量或表达式的值 |
q | 退出调试器,并终止正在调试的程序 |
h | 查看调试命令帮助 |
这些命令都需要在 (Pdb) 提示符后输入,而不是写进普通的 Python 代码文件中。
使用 n 逐步执行
假设程序暂停在下面这行代码之前:
1 | cpu_used = cpu_total - cpu_idle |
输入:
1 | n |
调试器就会执行当前显示的赋值语句,然后停在后续代码执行之前。
此时再输入:
1 | p cpu_used |
就可以看到:
1 | 80 |
通过这种方式,我们能够观察变量在程序执行过程中的变化。
需要注意,n 并不是执行一行就一定停在下一行源代码上。如果当前代码涉及循环、函数或其他执行流程,实际停留的位置会由程序的运行过程决定。
使用 s 进入函数内部
假设我们把 CPU 计算逻辑写成一个函数:
1 | def calculate_cpu(total, idle): |
程序暂停时,当前显示的待执行语句是:
1 | result = calculate_cpu(100, 20) |
输入:
1 | s |
如果当前执行位置是函数调用语句,s 可以进入 calculate_cpu() 函数内部,让我们检查函数内部的执行过程。
进入函数后,可以输入:
1 | p total |
输出:
1 | 100 |
再输入:
1 | p idle |
输出:
1 | 20 |
这样,我们就可以检查传入函数的参数是否正确。
如果使用 n 执行函数调用,调试器通常会运行完整个函数,然后停在调用语句之后;如果使用 s,则可以进入函数内部逐步检查。
使用 c 继续运行程序
如果已经完成检查,不需要再逐行执行,可以输入:
1 | c |
调试器就会继续运行程序,直到遇到下一个断点或程序结束。
对于简单程序,如果只有一个断点,执行 c 后通常就会直接运行到程序结束。
使用 l 查看代码
如果忘记当前程序执行到了哪里,可以输入:
1 | l |
调试器会显示当前代码附近的内容,帮助我们确认正在检查哪一段程序。
使用 q 退出调试
如果想主动结束调试,可以输入:
1 | q |
需要注意,q 会退出调试器,并终止当前正在调试的程序,而不是让程序正常执行完剩余代码。
使用 pdb 排查一个真实问题
了解了基本命令后,我们再来看一个更接近实际工作的例子。
假设我们正在编写一个简单的服务器 CPU 使用率计算程序:
1 | import pdb |
这段代码计算出的 CPU 使用率是 80.0%。
但如果程序输出了一个不符合预期的结果,我们应该怎么检查?
为了模拟这个场景,我们故意把空闲 CPU 数值改成 120:
1 | import pdb |
此时,计算出的 CPU 使用率会变成 -20.0%,显然不符合这个示例的业务预期。
运行程序后,调试器会暂停在 print() 语句执行之前。此时,前面的计算语句都已经执行完毕,因此可以直接检查各个变量。
在 (Pdb) 提示符下输入:
1 | p cpu_total |
输出:
1 | 100 |
再输入:
1 | p cpu_idle |
输出:
1 | 120 |
然后检查计算结果:
1 | p cpu_used |
输出:
1 | -20 |
最后检查使用率:
1 | p cpu_percent |
输出:
1 | -20.0 |
通过逐步检查,我们发现问题并不是 print() 输出错误,而是输入的空闲 CPU 数值已经超出了总量。
这个例子是为了演示调试过程而构造的。在真实的 Linux 服务器监控中,CPU 使用率通常需要根据 /proc/stat 等数据计算,不能直接把这里的简化公式当成完整的生产环境实现。
通过这个练习,我们可以总结出一个常见的调试思路:
flowchart TD
A["程序输出不符合预期"] --> B["设置调试断点"]
B --> C["查看输入变量"]
C --> D["逐步执行代码"]
D --> E["检查中间计算结果"]
E --> F{"找到异常数据或错误逻辑?"}
F -- "是" --> G["修正代码或输入数据"]
F -- "否" --> H["继续检查后续代码"]
H --> D
G --> I["重新运行并验证"]实际排查时,建议先检查输入数据,再检查中间变量,最后检查输出结果。这样比盲目修改代码更容易定位问题。
使用一行命令设置断点
前面的例子使用了两行代码导入和启动调试器:
1 | import pdb |
如果只是临时调试,也可以把它们写在同一行:
1 | import pdb; pdb.set_trace() |
这两种写法的作用相同。
其中,分号 ; 可以在同一行中分隔多条 Python 语句。
不过,对于刚开始学习 Python 的读者,我更建议使用分开的两行代码。这样更容易理解每条语句的作用,也更便于阅读和维护。
pdb 还有哪些使用注意事项
调试代码不等于修复代码
pdb 可以帮助我们发现问题,但不会自动修复程序。
找到错误原因后,仍然需要修改代码,并重新运行程序进行验证。
调试命令不是 Python 语句
进入 (Pdb) 提示符后,我们输入的是调试器命令,例如:
1 | n |
或者:
1 | p cpu_total |
这些命令是由 pdb 解释执行的,不是让我们直接写进 Python 文件的普通代码。
如果退出调试器,程序文件中的 Python 语句仍然按照正常的执行流程运行。
调试完成后清理临时断点
在代码中添加 pdb.set_trace() 后,每次程序执行到这里都会进入调试器。
因此,完成排查后,如果不希望程序再次暂停,应当删除或注释掉临时断点。
对于需要长期维护的程序,还应结合日志记录、异常处理和自动化测试等方式,建立更可靠的问题排查流程。
动手练习
现在,我们通过一个简单的服务器磁盘使用率示例练习 pdb。
创建文件 disk_debug.py:
1 | import pdb |
这里假设磁盘总容量为 500 GB,已使用容量为 425 GB。
运行程序:
1 | python disk_debug.py |
程序暂停后,注意当前显示的待执行语句是 print(...)。这是因为磁盘使用率的计算已经完成,而输出语句还没有执行。
尝试完成以下任务:
- 使用
p查看disk_total的值。 - 使用
p查看disk_used的值。 - 使用
p查看disk_percent的值。 - 使用
l查看当前代码。 - 使用
n执行输出语句,观察程序如何继续运行。 - 重新运行程序,使用
c直接继续运行。
完成后,再把 disk_used 修改为 475,重新运行程序,观察磁盘使用率的变化。
参考结果:
当磁盘总容量为 500 GB,已使用容量为 425 GB 时:
1 | (Pdb) p disk_percent |
当已使用容量修改为 475 GB 时,重新运行程序,使用率应为:
1 | 95.0 |
你也可以尝试故意修改计算公式,或者给变量赋予不同的值,然后利用 pdb 检查中间结果。通过主动制造问题并寻找原因,才能真正掌握调试的思路。
写在最后
这篇文章学习了 Python 标准库中的 pdb 调试工具,包括设置断点、查看变量、逐步执行代码、进入函数内部以及继续运行程序。
对于初学者来说,现阶段不需要记住所有调试命令。先熟练掌握 pdb.set_trace()、p、n、s、c 和 q,就可以应对很多基础调试场景。
还要记住一个关键点:执行到 pdb.set_trace() 时,断点语句本身已经执行完毕,调试器通常会暂停在后续代码执行之前。判断变量是否可以查看,需要结合程序实际执行到了哪一步。
当程序出现问题时,不要急着修改代码。先暂停程序,查看变量,观察执行过程,再根据实际结果判断问题出在哪里。
下一篇将学习如何交互式启动 Python,了解如何不创建 Python 文件,直接在命令行中执行代码并进行简单测试。
