在前面的学习中,我们已经知道如何使用 Python 标准库中的 pdb 模块调试程序。接下来,我们要学习一个非常实用的功能:设置断点,并在程序暂停时查看代码位置和变量值。

假设你正在编写一个 Linux 服务器巡检脚本,需要检查服务器的 CPU 使用率。当程序给出了不符合预期的结果时,你可能想知道:程序执行到了哪一行?变量里面保存的是什么值?为什么程序会进入某个判断分支?

这时,我们就可以使用断点来帮助分析问题。

什么是断点

断点可以理解为程序运行过程中的一个暂停位置。

当程序执行到设置好的断点时,pdb 会暂停程序,让我们检查当前的执行情况,例如:

  • 查看当前执行到哪一行代码。
  • 查看变量当前保存的值。
  • 查看当前代码附近的内容。
  • 检查条件表达式的计算结果。
  • 分析程序为什么进入某个判断分支。

需要注意,断点本身不会自动查找程序中的错误,它只是让程序暂停,方便我们检查现场。

准备一个 Python 示例程序

先创建一个 Python 文件,文件名为 server_check.py。

在文件中输入以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 模拟检查服务器 CPU 使用率

cpu_usage = 85
threshold = 80

print("开始检查服务器 CPU 使用率")

if cpu_usage > threshold:
status = "告警"
print("CPU 使用率过高")
else:
status = "正常"
print("CPU 使用率正常")

print("检查结果:", status)

这段程序模拟了一个简单的服务器巡检过程。

  • 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
2
3
开始检查服务器 CPU 使用率
CPU 使用率过高
检查结果: 告警

现在,我们希望在打印 CPU 告警信息之前暂停程序,检查当时的变量值。

启动 pdb 调试器

在终端中执行:

1
python -m pdb server_check.py

这里的 python -m pdb 表示使用 Python 启动标准库中的 pdb 调试器,server_check.py 是需要调试的程序文件。

调试器启动后,会在程序开始执行前暂停,并将当前位置指向第一条可执行语句。

由于示例程序的第 1 行是注释,第 2 行是空行,因此通常会停在第 3 行附近,显示类似下面的内容:

1
2
3
> /path/server_check.py(3)<module>()
-> cpu_usage = 85
(Pdb)

实际文件路径会因环境而异。

这里有几个需要理解的地方:

  • (3) 表示当前定位在第 3 行。
  • -> cpu_usage = 85 表示当前准备执行的代码。
  • (Pdb) 表示调试器正在等待我们输入命令。

此时,第 3 行还没有执行。 因此,变量 cpu_usage 尚未通过这条赋值语句获得值。

我们可以尝试查看它:

1
2
(Pdb) p cpu_usage
*** NameError: name 'cpu_usage' is not defined

这是因为当前程序还没有执行 cpu_usage = 85,所以变量尚未创建。这并不代表程序本身有错误,而是我们查看变量的时间太早了。

接下来,我们设置断点。

使用 b 命令设置断点

假设我们希望程序运行到第 10 行时暂停,以便检查告警分支的执行情况。

在 (Pdb) 提示符下输入:

1
(Pdb) b 10

这里的 b 是 break 的缩写,用于设置断点;数字 10 表示当前调试文件的第 10 行。

如果设置成功,会看到类似下面的输出:

1
2
Breakpoint 1 at /path/server_check.py:10
(Pdb)

其中:

  • 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
2
status = "告警"
print("CPU 使用率过高")

不过,当程序执行到第 10 行时,调试器会在这条 print() 语句执行之前暂停。

完整交互过程如下:

1
2
3
4
5
6
7
8
9
> /path/server_check.py(3)<module>()
-> cpu_usage = 85
(Pdb) b 10
Breakpoint 1 at /path/server_check.py:10
(Pdb) c
开始检查服务器 CPU 使用率
> /path/server_check.py(10)<module>()
-> print("CPU 使用率过高")
(Pdb)

现在,程序已经停在第 10 行。

注意,终端已经显示了“开始检查服务器 CPU 使用率”,因为第 6 行的打印语句已经执行完毕。

但是,终端还没有显示“CPU 使用率过高”,因为第 10 行的打印语句尚未执行。

这就是断点的作用:在指定语句执行之前暂停程序,让我们有机会检查当前状态。

查看当前代码位置

程序暂停后,输入:

1
(Pdb) l

l 是 list 的缩写,用于查看当前代码附近的内容。

输出大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
  5
6 print("开始检查服务器 CPU 使用率")
7
8 if cpu_usage > threshold:
9 status = "告警"
10 B-> print("CPU 使用率过高")
11 else:
12 status = "正常"
13 print("CPU 使用率正常")
14
15 print("检查结果:", status)
(Pdb)

实际显示的代码范围和格式可能略有不同。

其中:

  • B 表示该行设置了断点。
  • -> 表示当前准备执行的位置。
  • 第 10 行是当前暂停的位置。

通过这条命令,我们可以确认程序究竟停在哪里,以及接下来准备执行哪一行代码。

使用 p 命令查看变量值

接下来,我们想知道程序为什么进入告警分支。

可以使用 p 命令查看变量。

先查看 CPU 使用率:

1
2
(Pdb) p cpu_usage
85

输出的 85 表示变量 cpu_usage 当前保存的值。

再查看告警阈值:

1
2
(Pdb) p threshold
80

输出的 80 表示变量 threshold 当前保存的值。

还可以直接检查条件表达式:

1
2
(Pdb) p cpu_usage > threshold
True

为什么结果是 True?

因为:

1
85 > 80

这个条件成立,所以 Python 返回布尔值 True。

这说明程序进入 if 分支是符合预期的。

我们还可以查看 status:

1
2
(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
2
3
> /path/server_check.py(3)<module>()
-> cpu_usage = 85
(Pdb)

输入 b 10 设置断点:

1
2
3
(Pdb) b 10
Breakpoint 1 at /path/server_check.py:10
(Pdb)

输入 c 继续执行:

1
2
3
4
5
(Pdb) c
开始检查服务器 CPU 使用率
> /path/server_check.py(10)<module>()
-> print("CPU 使用率过高")
(Pdb)

程序停在第 10 行。接下来,查看变量:

1
2
3
4
5
6
7
8
(Pdb) p cpu_usage
85
(Pdb) p threshold
80
(Pdb) p cpu_usage > threshold
True
(Pdb) p status
'告警'

通过这些结果,我们已经确认:

  • CPU 使用率是 85。
  • 告警阈值是 80。
  • 条件表达式的结果为 True。
  • status 已经被设置为 "告警"。
  • 第 10 行的打印语句尚未执行。

最后,输入 c,让程序继续运行:

1
2
3
(Pdb) c
CPU 使用率过高
检查结果: 告警

此时程序已经执行完剩余代码。

至此,我们完成了从启动调试器、设置断点、检查变量到继续运行结束的完整过程。

为什么第 10 行断点有时不会命中

这是学习断点时非常重要的一点。

断点能否命中,取决于程序实际执行路径是否经过设置断点的位置。

我们用同一份程序验证两种情况。

情况一:CPU 使用率为 85

代码中的变量为:

1
2
cpu_usage = 85
threshold = 80

判断条件为:

1
if cpu_usage > threshold:

因为 85 > 80,条件成立,程序进入 if 分支。

执行过程如下:

1
2
3
执行第 8 行:判断条件
执行第 9 行:status = "告警"
到达第 10 行:命中断点并暂停

因此,第 10 行断点会命中。

此时,status 已经是 "告警",但第 10 行的 print() 还没有执行。

情况二:CPU 使用率为 65

现在,将代码中的变量修改为:

1
2
cpu_usage = 65
threshold = 80

保存文件后,重新启动调试器:

1
python -m pdb server_check.py

调试器首次暂停后,输入:

1
2
3
(Pdb) b 10
Breakpoint 1 at /path/server_check.py:10
(Pdb) c

因为 65 > 80 的结果是 False,程序不会进入 if 分支,而是直接进入 else 分支。

因此,第 10 行不会被执行,断点也就不会命中。

程序会继续执行,并输出:

1
2
3
开始检查服务器 CPU 使用率
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

每次都按照以下步骤操作:

  1. 在首次暂停时输入 b 10。
  2. 输入 c,让程序继续运行。
  3. 观察程序是否停在第 10 行。
  4. 如果停住了,输入 p cpu_usage。
  5. 输入 p threshold。
  6. 输入 p cpu_usage > threshold,查看条件表达式的结果。
  7. 输入 c,观察程序剩余部分的执行过程。

完成练习后,尝试回答以下问题:

  • 为什么 cpu_usage 为 85 时,第 10 行断点会命中?
  • 为什么 cpu_usage 为 65 时,第 10 行断点不会命中?
  • 为什么程序停在第 10 行时,status 已经有值,但告警信息还没有打印出来?
  • 设置断点之后,为什么还需要输入 c?

如果能够回答这些问题,就说明你已经掌握了断点的基本工作方式,以及如何在程序暂停时检查当前上下文。

写在最后

本篇文章学习了如何使用 pdb 设置断点、继续执行程序,以及在断点处查看代码位置、变量值和条件表达式的结果。

在实际的 Linux 运维自动化脚本中,这些能力可以帮助我们排查服务器巡检结果不正确、告警条件判断异常等问题。

下一篇将继续学习如何管理断点,包括查看断点列表、删除指定断点,以及删除断点后程序的实际执行过程。