Python 系列(六十五) 使用 pdb 设置断点并检查程序执行上下文
在前面的学习中,我们已经知道如何使用 Python 标准库中的 pdb 模块调试程序。接下来,我们要学习一个非常实用的功能:设置断点,并在程序暂停时查看代码位置和变量值。
假设你正在编写一个 Linux 服务器巡检脚本,需要检查服务器的 CPU 使用率。当程序给出了不符合预期的结果时,你可能想知道:程序执行到了哪一行?变量里面保存的是什么值?为什么程序会进入某个判断分支?
这时,我们就可以使用断点来帮助分析问题。
什么是断点
断点可以理解为程序运行过程中的一个暂停位置。
当程序执行到设置好的断点时,pdb 会暂停程序,让我们检查当前的执行情况,例如:
- 查看当前执行到哪一行代码。
- 查看变量当前保存的值。
- 查看当前代码附近的内容。
- 检查条件表达式的计算结果。
- 分析程序为什么进入某个判断分支。
需要注意,断点本身不会自动查找程序中的错误,它只是让程序暂停,方便我们检查现场。
准备一个 Python 示例程序
先创建一个 Python 文件,文件名为 server_check.py。
在文件中输入以下代码:
1 | # 模拟检查服务器 CPU 使用率 |
这段程序模拟了一个简单的服务器巡检过程。
cpu_usage表示当前 CPU 使用率,这里设置为85。threshold表示告警阈值,这里设置为80。if语句负责判断 CPU 使用率是否超过阈值。status用于保存最终的检查结果。
为了方便后续调试,我们先了解程序中的关键行号。
| 行号 | 代码作用 |
|---|---|
| 第 3 行 | 设置 CPU 使用率为 85 |
| 第 4 行 | 设置告警阈值为 80 |
| 第 6 行 | 打印开始检查的信息 |
| 第 8 行 | 判断 CPU 使用率是否超过阈值 |
| 第 9 行 | 将检查结果设置为告警 |
| 第 10 行 | 打印 CPU 告警信息 |
| 第 12 行 | 将检查结果设置为正常 |
| 第 13 行 | 打印 CPU 正常信息 |
| 第 15 行 | 打印最终检查结果 |
保存文件后,在终端中执行:
1 | python server_check.py |
预期输出:
1 | 开始检查服务器 CPU 使用率 |
现在,我们希望在打印 CPU 告警信息之前暂停程序,检查当时的变量值。
启动 pdb 调试器
在终端中执行:
1 | python -m pdb server_check.py |
这里的 python -m pdb 表示使用 Python 启动标准库中的 pdb 调试器,server_check.py 是需要调试的程序文件。
调试器启动后,会在程序开始执行前暂停,并将当前位置指向第一条可执行语句。
由于示例程序的第 1 行是注释,第 2 行是空行,因此通常会停在第 3 行附近,显示类似下面的内容:
1 | > /path/server_check.py(3)<module>() |
实际文件路径会因环境而异。
这里有几个需要理解的地方:
(3)表示当前定位在第 3 行。-> cpu_usage = 85表示当前准备执行的代码。(Pdb)表示调试器正在等待我们输入命令。
此时,第 3 行还没有执行。 因此,变量 cpu_usage 尚未通过这条赋值语句获得值。
我们可以尝试查看它:
1 | (Pdb) p cpu_usage |
这是因为当前程序还没有执行 cpu_usage = 85,所以变量尚未创建。这并不代表程序本身有错误,而是我们查看变量的时间太早了。
接下来,我们设置断点。
使用 b 命令设置断点
假设我们希望程序运行到第 10 行时暂停,以便检查告警分支的执行情况。
在 (Pdb) 提示符下输入:
1 | (Pdb) b 10 |
这里的 b 是 break 的缩写,用于设置断点;数字 10 表示当前调试文件的第 10 行。
如果设置成功,会看到类似下面的输出:
1 | Breakpoint 1 at /path/server_check.py:10 |
其中:
Breakpoint 1表示调试器为这个断点分配的编号是1。server_check.py:10表示断点位于文件的第 10 行。
需要注意,设置断点不会让程序立即跳转到第 10 行。此时程序仍然停留在最初的第 3 行。
断点只是提前告诉调试器:如果程序后续执行到第 10 行,就在这里暂停。
使用 c 命令继续运行程序
断点设置完成后,输入:
1 | (Pdb) c |
c 是 continue 的缩写,表示继续执行程序,直到遇到断点或程序结束。
因为当前设置的 cpu_usage 为 85,threshold 为 80,所以条件成立:
1 | if cpu_usage > threshold: |
程序会进入 if 分支,并依次执行:
1 | status = "告警" |
不过,当程序执行到第 10 行时,调试器会在这条 print() 语句执行之前暂停。
完整交互过程如下:
1 | > /path/server_check.py(3)<module>() |
现在,程序已经停在第 10 行。
注意,终端已经显示了“开始检查服务器 CPU 使用率”,因为第 6 行的打印语句已经执行完毕。
但是,终端还没有显示“CPU 使用率过高”,因为第 10 行的打印语句尚未执行。
这就是断点的作用:在指定语句执行之前暂停程序,让我们有机会检查当前状态。
查看当前代码位置
程序暂停后,输入:
1 | (Pdb) l |
l 是 list 的缩写,用于查看当前代码附近的内容。
输出大致如下:
1 | 5 |
实际显示的代码范围和格式可能略有不同。
其中:
B表示该行设置了断点。->表示当前准备执行的位置。- 第 10 行是当前暂停的位置。
通过这条命令,我们可以确认程序究竟停在哪里,以及接下来准备执行哪一行代码。
使用 p 命令查看变量值
接下来,我们想知道程序为什么进入告警分支。
可以使用 p 命令查看变量。
先查看 CPU 使用率:
1 | (Pdb) p cpu_usage |
输出的 85 表示变量 cpu_usage 当前保存的值。
再查看告警阈值:
1 | (Pdb) p threshold |
输出的 80 表示变量 threshold 当前保存的值。
还可以直接检查条件表达式:
1 | (Pdb) p cpu_usage > threshold |
为什么结果是 True?
因为:
1 | 85 > 80 |
这个条件成立,所以 Python 返回布尔值 True。
这说明程序进入 if 分支是符合预期的。
我们还可以查看 status:
1 | (Pdb) p status |
为什么此时 status 已经有值了?
因为程序在到达第 10 行之前,已经执行过第 9 行:
1 | status = "告警" |
因此,虽然第 10 行的打印语句还没有执行,变量 status 已经保存了字符串 "告警"。
完整演示:从启动到结束
现在,我们把整个过程串起来,完成一次完整的调试。
首先确认 server_check.py 中的 cpu_usage 为 85,然后在终端中执行:
1 | python -m pdb server_check.py |
调试器启动后,会在第 3 行附近暂停:
1 | > /path/server_check.py(3)<module>() |
输入 b 10 设置断点:
1 | (Pdb) b 10 |
输入 c 继续执行:
1 | (Pdb) c |
程序停在第 10 行。接下来,查看变量:
1 | (Pdb) p cpu_usage |
通过这些结果,我们已经确认:
- CPU 使用率是
85。 - 告警阈值是
80。 - 条件表达式的结果为
True。 status已经被设置为"告警"。- 第 10 行的打印语句尚未执行。
最后,输入 c,让程序继续运行:
1 | (Pdb) c |
此时程序已经执行完剩余代码。
至此,我们完成了从启动调试器、设置断点、检查变量到继续运行结束的完整过程。
为什么第 10 行断点有时不会命中
这是学习断点时非常重要的一点。
断点能否命中,取决于程序实际执行路径是否经过设置断点的位置。
我们用同一份程序验证两种情况。
情况一:CPU 使用率为 85
代码中的变量为:
1 | cpu_usage = 85 |
判断条件为:
1 | if cpu_usage > threshold: |
因为 85 > 80,条件成立,程序进入 if 分支。
执行过程如下:
1 | 执行第 8 行:判断条件 |
因此,第 10 行断点会命中。
此时,status 已经是 "告警",但第 10 行的 print() 还没有执行。
情况二:CPU 使用率为 65
现在,将代码中的变量修改为:
1 | cpu_usage = 65 |
保存文件后,重新启动调试器:
1 | python -m pdb server_check.py |
调试器首次暂停后,输入:
1 | (Pdb) b 10 |
因为 65 > 80 的结果是 False,程序不会进入 if 分支,而是直接进入 else 分支。
因此,第 10 行不会被执行,断点也就不会命中。
程序会继续执行,并输出:
1 | 开始检查服务器 CPU 使用率 |
程序结束后,调试器会显示程序退出相关信息。具体提示可能因 Python 版本和调试器状态而有所不同。
需要明确的是:断点仍然存在,只是这次程序的执行路径没有经过第 10 行。
如果程序在到达断点之前就结束了,或者执行了其他分支,断点都可能不会触发。
理解断点调试的执行流程
flowchart TD
A["启动 pdb 调试器"] --> B["首次暂停在第 3 行"]
B --> C["输入 b 10 设置断点"]
C --> D["输入 c 继续运行"]
D --> E{"cpu_usage 是否大于 threshold"}
E -->|是| F["进入 if 分支"]
F --> G["执行第 9 行赋值"]
G --> H["到达第 10 行"]
H --> I["命中断点并暂停"]
I --> J["使用 l 查看代码"]
I --> K["使用 p 查看变量"]
E -->|否| L["进入 else 分支"]
L --> M["跳过第 10 行"]
M --> N["继续执行并结束"]这张流程图说明,断点调试不仅取决于设置了哪个行号,还取决于程序实际走了哪条执行路径。
动手练习
请自己完成下面的练习。
分别将 cpu_usage 设置为 85 和 65,每次修改后保存文件,并重新启动调试器:
1 | python -m pdb server_check.py |
每次都按照以下步骤操作:
- 在首次暂停时输入
b 10。 - 输入
c,让程序继续运行。 - 观察程序是否停在第 10 行。
- 如果停住了,输入
p cpu_usage。 - 输入
p threshold。 - 输入
p cpu_usage > threshold,查看条件表达式的结果。 - 输入
c,观察程序剩余部分的执行过程。
完成练习后,尝试回答以下问题:
- 为什么
cpu_usage为85时,第 10 行断点会命中? - 为什么
cpu_usage为65时,第 10 行断点不会命中? - 为什么程序停在第 10 行时,
status已经有值,但告警信息还没有打印出来? - 设置断点之后,为什么还需要输入
c?
如果能够回答这些问题,就说明你已经掌握了断点的基本工作方式,以及如何在程序暂停时检查当前上下文。
写在最后
本篇文章学习了如何使用 pdb 设置断点、继续执行程序,以及在断点处查看代码位置、变量值和条件表达式的结果。
在实际的 Linux 运维自动化脚本中,这些能力可以帮助我们排查服务器巡检结果不正确、告警条件判断异常等问题。
下一篇将继续学习如何管理断点,包括查看断点列表、删除指定断点,以及删除断点后程序的实际执行过程。
