编写 Python 程序时,我们经常会遇到这样的情况:程序可以运行,但是输出结果不符合预期;或者程序运行到一半就报错了,我们却不知道问题出在哪里。

例如,我们希望计算服务器的 CPU 使用率,却发现最终结果不对。检查代码时,每一行看起来都没有明显的问题,该怎么办?

如果只能通过添加 print() 语句来查看变量的值,不仅需要反复修改代码,还可能需要添加很多调试信息。

Python 提供了一个内置的调试工具:pdb。它属于 Python 标准库,不需要额外安装,可以帮助我们暂停程序、逐步执行代码、查看变量的值,从而找到程序中的问题。

这篇文章将从零开始,学习如何使用 pdb 调试 Python 程序。

认识 pdb

pdb 是 Python 提供的交互式调试器,可以让我们在程序运行过程中暂停执行,并通过输入调试命令观察程序的运行状态。

可以先把调试理解成:让程序暂时停下来,一步一步检查它到底在做什么。

例如,假设我们正在编写一个统计服务器 CPU 使用率的程序:

1
2
3
4
5
6
cpu_total = 100
cpu_idle = 20

cpu_used = cpu_total - cpu_idle

print(f"CPU 使用率:{cpu_used}%")

运行程序:

1
python debug_demo.py

输出结果:

1
CPU 使用率:80%

这个程序可以正常运行,但如果我们发现结果不符合预期,就需要检查变量的值,以及计算过程是否正确。

我们可以使用 pdb,让程序在执行到指定位置时暂停下来。

创建第一个调试程序

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

在文件中输入以下代码:

1
2
3
4
5
6
7
8
9
10
import pdb

cpu_total = 100
cpu_idle = 20

pdb.set_trace()

cpu_used = cpu_total - cpu_idle

print(f"CPU 使用率:{cpu_used}%")

逐行认识这段代码。

  • 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
2
3
> debug_demo.py(7)<module>()
-> cpu_used = cpu_total - cpu_idle
(Pdb)

这里假设 pdb.set_trace() 位于第 6 行,具体行号可能因文件内容不同而变化。

这段信息包含几个重要内容:

  • debug_demo.py 表示当前调试的文件。
  • (7) 表示调试器当前显示的代码位置是文件中的第 7 行。
  • -> cpu_used = cpu_total - cpu_idle 表示接下来准备执行的代码语句。
  • (Pdb) 是调试器的命令提示符,表示程序已经暂停,正在等待我们输入调试命令。

因此,虽然程序是通过 pdb.set_trace() 进入调试器的,但当前显示的待执行语句通常是断点后面的下一条语句,而不是 pdb.set_trace() 本身。

断点后的完整交互过程

为了真正掌握 pdb,我们先完整地操作一遍,看看程序暂停后应该输入什么命令,以及每一步会发生什么。

当前调试的程序是:

1
2
3
4
5
6
7
8
9
10
import pdb

cpu_total = 100
cpu_idle = 20

pdb.set_trace()

cpu_used = cpu_total - cpu_idle

print(f"CPU 使用率:{cpu_used}%")

运行程序后,调试器暂停在 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
2
3
> debug_demo.py(9)<module>()
-> print(f"CPU 使用率:{cpu_used}%")
(Pdb)

这里的行号仅为示例。

此时,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
C:\python-demo>python debug_demo.py
> debug_demo.py(7)<module>()
-> cpu_used = cpu_total - cpu_idle
(Pdb) p cpu_total
100
(Pdb) p cpu_idle
20
(Pdb) p cpu_used
*** NameError: name 'cpu_used' is not defined
(Pdb) n
> debug_demo.py(9)<module>()
-> print(f"CPU 使用率:{cpu_used}%")
(Pdb) p cpu_used
80
(Pdb) p cpu_used / cpu_total * 100
80.0
(Pdb) n
CPU 使用率:80%

以上是示意交互记录。实际显示的路径、行号和提示信息可能因操作系统、Python 版本及代码内容而有所不同。

通过这个过程,我们可以看到 pdb 的基本工作方式:

  1. 程序运行到 pdb.set_trace(),进入调试器。
  2. 使用 p 检查已经存在的变量。
  3. 使用 n 执行当前待执行语句。
  4. 再次使用 p 检查执行后的变量状态。
  5. 继续使用 n 执行剩余语句,直到程序结束。

需要注意,程序正常结束后,调试器通常会自动退出,因此不需要再输入 q。

查看变量的值

前面的完整交互已经介绍了 p 命令。这里再单独总结一下它的用途。

p 是 print 的简写形式,可以在调试过程中输出变量或表达式的结果。

例如:

1
2
3
4
5
6
(Pdb) p cpu_total
100
(Pdb) p cpu_idle
20
(Pdb) p cpu_total - cpu_idle
80

前两条命令分别查看变量的值,第三条命令直接计算两个变量相减的结果。

如果程序输出不符合预期,可以先检查参与计算的变量是否正确,再检查计算表达式本身。

需要注意,变量必须已经在当前作用域中定义,才能直接通过变量名查看其值。如果赋值语句还没有执行,访问该变量就可能出现 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
2
3
4
5
6
7
8
9
10
11
12
def calculate_cpu(total, idle):
used = total - idle
return used


import pdb

pdb.set_trace()

result = calculate_cpu(100, 20)

print(result)

程序暂停时,当前显示的待执行语句是:

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
2
3
4
5
6
7
8
9
10
11
import pdb

cpu_total = 100
cpu_idle = 20

cpu_used = cpu_total - cpu_idle
cpu_percent = cpu_used / cpu_total * 100

pdb.set_trace()

print(f"CPU 使用率:{cpu_percent}%")

这段代码计算出的 CPU 使用率是 80.0%。

但如果程序输出了一个不符合预期的结果,我们应该怎么检查?

为了模拟这个场景,我们故意把空闲 CPU 数值改成 120:

1
2
3
4
5
6
7
8
9
10
11
import pdb

cpu_total = 100
cpu_idle = 120

cpu_used = cpu_total - cpu_idle
cpu_percent = cpu_used / cpu_total * 100

pdb.set_trace()

print(f"CPU 使用率:{cpu_percent}%")

此时,计算出的 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
2
3
import pdb

pdb.set_trace()

如果只是临时调试,也可以把它们写在同一行:

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
2
3
4
5
6
7
8
9
10
import pdb

disk_total = 500
disk_used = 425

disk_percent = disk_used / disk_total * 100

pdb.set_trace()

print(f"磁盘使用率:{disk_percent}%")

这里假设磁盘总容量为 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
2
(Pdb) p disk_percent
85.0

当已使用容量修改为 475 GB 时,重新运行程序,使用率应为:

1
95.0

你也可以尝试故意修改计算公式,或者给变量赋予不同的值,然后利用 pdb 检查中间结果。通过主动制造问题并寻找原因,才能真正掌握调试的思路。

写在最后

这篇文章学习了 Python 标准库中的 pdb 调试工具,包括设置断点、查看变量、逐步执行代码、进入函数内部以及继续运行程序。

对于初学者来说,现阶段不需要记住所有调试命令。先熟练掌握 pdb.set_trace()、p、n、s、c 和 q,就可以应对很多基础调试场景。

还要记住一个关键点:执行到 pdb.set_trace() 时,断点语句本身已经执行完毕,调试器通常会暂停在后续代码执行之前。判断变量是否可以查看,需要结合程序实际执行到了哪一步。

当程序出现问题时,不要急着修改代码。先暂停程序,查看变量,观察执行过程,再根据实际结果判断问题出在哪里。

下一篇将学习如何交互式启动 Python,了解如何不创建 Python 文件,直接在命令行中执行代码并进行简单测试。