Python 系列(六十三) 在交互式 Python shell 中启动调试器
前面我们学习了使用 Python 自带的 pdb 模块调试程序,也接触了 pdb.set_trace() 这种设置断点的方法。
例如,当服务器巡检程序发现 CPU 使用率异常时,我们可能想知道:程序读取的 CPU 使用率是多少?为什么进入了告警分支?是哪一行代码产生了问题?
如果为了调试而修改源代码、添加断点,调试完成后还要清理这些代码,就会多出一些工作。
其实,Python 提供了另一种更直接的方式:在命令行中启动 pdb 调试器,不修改源代码也能逐步检查程序的执行过程。
这篇文章将从一个完整的示例开始,带你实际操作命令行调试。
准备一个需要调试的 Python 程序
假设我们正在编写一个服务器巡检程序,需要检查 CPU 和内存的使用率。
新建一个名为 server_check.py 的文件,写入以下代码:
1 | # 模拟服务器的资源使用率 |
这段程序模拟了服务器资源检查的过程。
cpu_usage = 72:将 CPU 使用率设置为 72。memory_usage = 88:将内存使用率设置为 88。if cpu_usage > 80:判断 CPU 使用率是否超过 80。if memory_usage > 80:判断内存使用率是否超过 80。- 最后一条
print():输出检查完成的信息。
这里的 CPU 和内存使用率都是我们手动设置的模拟数据,并没有真正读取服务器的资源使用情况。
保存文件后,在终端中进入文件所在的目录。
先正常运行一次程序:
1 | python server_check.py |
预期输出:
1 | 开始检查服务器资源 |
CPU 使用率为 72,没有超过 80,所以程序输出 CPU 使用率正常。
内存使用率为 88,超过了 80,所以程序输出内存使用率过高。
如果我们希望观察程序是如何得出这些结果的,就可以启动调试器。
使用 python -m pdb 启动调试器
在终端中执行:
1 | python -m pdb server_check.py |
这条命令可以拆成三个部分理解:
| 命令 | 含义 |
|---|---|
python | 启动 Python 解释器 |
-m pdb | 通过模块方式启动 Python 自带的 pdb 调试器 |
server_check.py | 指定需要调试的 Python 文件 |
简单来说,这条命令就是让 Python 使用 pdb 调试器运行 server_check.py。
这里不需要在源代码中添加 import pdb 或 pdb.set_trace()。
第一次启动时,调试器会做什么?
执行命令后,终端通常会显示类似下面的内容:
1 | > /path/to/server_check.py(2)<module>() |
示例中的 /path/to/server_check.py 只是占位路径,实际显示的是你自己的文件路径。
这里的 (2) 表示调试器当前关联的是文件第 2 行。由于第 1 行是注释,调试器通常会停在第一条可执行语句 cpu_usage = 72 上。
关键是:首次启动时,调试器会先让你获得控制权,而不是直接把整个脚本运行完。 当前显示的代码通常还没有执行。
我们逐项理解终端中的信息。
1 | > /path/to/server_check.py(2)<module>() |
这部分表示当前调试的文件、代码行号,以及正在执行的代码所属的作用域。<module>() 表示模块顶层代码,而不是某个自定义函数。
1 | -> cpu_usage = 72 |
箭头 -> 指向当前准备执行的代码。
此时,赋值语句还没有执行,因此变量 cpu_usage 尚未通过这行代码创建。
1 | (Pdb) |
这是调试器的命令提示符,表示程序已经暂停在调试状态,正在等待我们输入命令。
这里还有一个容易混淆的地方:pdb 不会因为停在第一条语句前,就自动把所有变量准备好。变量需要等到对应的赋值语句真正执行之后才能读取。
从首次暂停开始,完整调试程序
下面按照实际操作顺序,观察程序如何从启动、暂停、逐行执行,一直到结束。
查看尚未创建的变量
当终端显示:
1 | (Pdb) |
时,输入:
1 | p cpu_usage |
通常会得到类似结果:
1 | *** NameError: name 'cpu_usage' is not defined |
原因很简单:程序还没有执行 cpu_usage = 72,变量自然还不存在。
这不是程序出错,而是我们查看变量的时间太早了。
执行第一条赋值语句
输入:
1 | n |
n 是 next 的缩写,表示执行当前语句,然后停在下一条将要执行的语句处。
当前语句:
1 | cpu_usage = 72 |
执行完成后,调试器会停在后续代码的位置,通常是下一条赋值语句:
1 | memory_usage = 88 |
这时再输入:
1 | p cpu_usage |
预期输出:
1 | 72 |
说明变量已经创建,值为 72。
继续执行下一行:
1 | n |
现在 memory_usage = 88 也已执行。
输入:
1 | p memory_usage |
预期输出:
1 | 88 |
通过这种方式,我们可以在程序运行过程中检查变量,而不必额外添加 print() 语句。
观察程序执行输出语句
继续输入:
1 | n |
多次执行 n,直到调试器停在:
1 | print("开始检查服务器资源") |
输入:
1 | n |
程序执行这条输出语句,终端显示:
1 | 开始检查服务器资源 |
调试器随后会停在接下来将要执行的语句处。
请注意,调试器显示的箭头表示当前准备执行的代码,而不是最近已经执行完的代码。因此,输入一次 n 后,看到箭头移动到下一条语句是正常现象。
观察 CPU 判断分支
继续使用 n,直到调试器停在:
1 | if cpu_usage > 80: |
先查看变量:
1 | p cpu_usage |
输出:
1 | 72 |
再查看条件表达式:
1 | p cpu_usage > 80 |
输出:
1 | False |
False 表示条件不成立。
因为 72 不大于 80,程序不会执行 if 后面的告警语句,而是进入 else 分支。
输入:
1 | n |
调试器会继续按照程序的实际执行路径移动。当箭头指向:
1 | print("CPU 使用率正常") |
再输入:
1 | n |
终端就会输出:
1 | CPU 使用率正常 |
通过这个过程,我们不仅知道最终输出了什么,还能看到程序为什么会产生这样的结果。
观察内存判断分支
继续使用 n,直到调试器停在:
1 | if memory_usage > 80: |
输入:
1 | p memory_usage |
输出:
1 | 88 |
再检查判断条件:
1 | p memory_usage > 80 |
输出:
1 | True |
True 表示条件成立。
因为内存使用率 88 大于 80,程序会执行 if 分支中的告警语句。
继续输入:
1 | n |
当箭头指向:
1 | print("内存使用率过高") |
再次输入:
1 | n |
终端输出:
1 | 内存使用率过高 |
通过这几步,我们观察到了两种不同的条件判断结果:
- CPU 使用率为
72,判断结果为False,进入else分支。 - 内存使用率为
88,判断结果为True,进入if分支。
这就是调试器最实用的地方之一:不仅能查看变量,还能跟踪程序实际走了哪条执行路径。
使用 c 继续运行程序
如果不想再逐行执行,可以输入:
1 | c |
c 是 continue 的缩写,表示继续运行程序,直到程序结束或遇到其他调试暂停位置。
假设前面的代码已经执行完毕,调试器当前停在最后一条输出语句:
1 | print("服务器检查完成") |
输入 c 后,程序会继续执行,并输出:
1 | 服务器检查完成 |
如果你在刚启动调试器时就输入 c,则程序会从当前暂停的位置继续执行,通常会一次性执行完剩余代码。
程序结束后为什么可能再次运行?
这是使用 python -m pdb 时尤其值得注意的地方。
当程序正常结束后,pdb 默认会重新启动被调试的程序,以便用户可以再次进行调试。因此,你可能看到类似这样的终端输出:
1 | 服务器检查完成 |
其中,英文提示表示程序已经结束,调试器准备重新启动它。
紧接着再次出现的代码位置和 (Pdb) 提示符,代表新一轮调试已经开始,程序又停在第一条可执行语句附近。
这不代表程序陷入了死循环,也不代表 n 或 c 失效,而是 pdb 默认的重新启动行为。
如果只是想完成一次调试,不需要重新运行程序,可以使用下面介绍的 q 命令退出。
使用 q 退出调试器
当终端重新出现 (Pdb) 提示符时,输入:
1 | q |
q 是 quit 的缩写,用于退出调试器。
退出后,就会返回普通的终端命令提示符。
这样,一次完整的命令行调试就结束了。
常用调试命令回顾
| 命令 | 作用 | 使用场景 |
|---|---|---|
p 表达式 | 查看表达式的值 | 查看变量、判断条件的结果 |
n | 执行下一行,不进入被调用函数内部逐行调试 | 逐步观察程序执行 |
s | 执行当前语句,并在函数调用时进入函数内部 | 检查函数内部逻辑 |
c | 继续运行程序 | 完成检查后继续执行 |
q | 退出调试器 | 结束本次调试 |
例如:
1 | p cpu_usage |
这些命令都在 (Pdb) 提示符下输入,而不是在普通的 Windows 命令提示符或 Linux Shell 中单独执行。
其中,s 是 step 的缩写。它与 n 的主要区别在于:如果当前代码调用了函数,s 可以进入函数内部继续调试,而 n 通常会执行完这次函数调用,再停在调用位置之后的代码处。
动手练习
请把下面的代码保存为 server_check.py:
1 | cpu_usage = 65 |
按照以下步骤练习:
- 使用
python -m pdb server_check.py启动调试器。 - 在首次暂停时,尝试查看
cpu_usage,理解为什么它还没有定义。 - 使用
n执行赋值语句。 - 使用
p cpu_usage和p memory_usage查看变量值。 - 使用
p cpu_usage > 80和p memory_usage > 80查看判断结果。 - 使用
n观察程序如何进入不同的条件分支。 - 使用
c继续运行程序。 - 如果程序结束后调试器重新启动,请使用
q退出。
参考结果:
1 | 65 |
这四个结果分别对应两个变量的值,以及两个条件判断的结果。
如果你能够独立完成这些操作,就已经掌握了使用命令行启动调试器、查看变量和跟踪条件分支的基本方法。
写在最后
本篇介绍了如何使用 python -m pdb script.py 在命令行中启动 Python 调试器,并通过服务器资源检查示例,演示了首次暂停、查看变量、逐行执行、检查条件分支、继续运行和退出调试器的完整过程。
需要记住两个关键点:
- 首次启动时,
pdb通常会在第一条可执行语句处暂停,等待我们输入调试命令。 - 程序结束后,
pdb默认可能重新启动脚本;如果不需要再次调试,可以使用q退出。
下一篇将继续学习 pdb 的常用调试命令,进一步掌握设置断点、查看调用栈和控制程序执行的方法。
