如何使用 Python 调试器

简介

在软件开发中,调试 是查找并解决阻止软件正确运行的问题的过程。

Python 调试器为 Python 程序提供了一个调试环境。它支持设置条件断点、逐行浏览源代码、堆栈检查等功能。

先决条件

您应该已经安装了 Python 3,并在计算机或服务器上设置了编程环境。如果您还没有设置编程环境,可以参考适用于您操作系统(Ubuntu、CentOS、Debian 等)的本地编程环境或服务器编程环境的安装和设置指南。

与 Python 调试器的交互工作

Python 调试器作为 Python 标准发行版的一部分,以一个名为 pdb 的模块的形式提供。调试器也是可扩展的,定义为 Pdb 类。您可以阅读 pdb 的官方文档以了解更多信息。

我们将从一个简短的程序开始,该程序有两个全局变量、一个创建嵌套循环的函数,以及调用 nested_loop() 函数的 if __name__ == '__main__': 结构。

num_list = [500, 600, 700]
alpha_list = ['x', 'y', 'z']


def nested_loop():
    for number in num_list:
        print(number)
        for letter in alpha_list:
            print(letter)

if __name__ == '__main__':
    nested_loop()

我们可以通过以下命令使用 Python 调试器运行此程序:

python -m pdb looping.py

-m 命令行标志将为您导入任何 Python 模块并将其作为脚本运行。在这种情况下,我们正在导入并运行 pdb 模块,如上所示。

运行此命令后,您将收到以下输出:

> /Users/sammy/looping.py(1)<module>()
-> num_list = [500, 600, 700]
(Pdb) 

在输出中,第一行包含当前模块名称(如 <module> 所示)和目录路径,以及随后的打印行号(在本例中为 1,但如果有注释或其他不可执行行,则可能是更高的数字)。第二行显示了在此处执行的源代码的当前行,因为 pdb 提供了一个用于调试的交互式控制台。您可以使用 help 命令来了解其命令,使用 help command 来了解有关特定命令的更多信息。请注意,pdb 控制台与 Python 交互式 shell 不同。

当 Python 调试器到达程序末尾时,它将自动重新开始。每当您想要离开 pdb 控制台时,请键入 quitexit 命令。如果您想要在程序的任何位置显式重新启动程序,可以使用 run 命令。

使用调试器浏览程序

在使用 Python 调试器处理程序时,您可能会使用 liststepnext 命令逐行浏览代码。在本节中,我们将介绍这些命令。

在 shell 中,我们可以输入 list 命令以获取当前行的上下文。从上面显示的程序 looping.py 的第一行 — num_list = [500, 600, 700] — 将显示如下内容:

(Pdb) list
  1  ->	num_list = [500, 600, 700]
  2  	alpha_list = ['x', 'y', 'z']
  3  	
  4  	
  5  	def nested_loop():
  6  	    for number in num_list:
  7  	        print(number)
  8  	        for letter in alpha_list:
  9  	            print(letter)
 10  	
 11  	if __name__ == '__main__':
(Pdb) 

当前行用 -> 字符表示,这在我们的情况下是程序文件的第一行。

由于这是一个相对较短的程序,我们几乎可以通过 list 命令获得整个程序。如果不提供参数,list 命令将提供当前行周围的 11 行,但您也可以指定要包括的行,如下所示:

(Pdb) list 3, 7
  3  	
  4  	
  5  	def nested_loop():
  6  	    for number in num_list:
  7  	        print(number)
(Pdb) 

在这里,我们使用 list 3, 7 命令请求显示行 3-7。

要逐行浏览程序,我们可以使用 stepnext

(Pdb) step
> /Users/sammy/looping.py(2)<module>()
-> alpha_list = ['x', 'y', 'z']
(Pdb) 
(Pdb) next
> /Users/sammy/looping.py(2)<module>()
-> alpha_list = ['x', 'y', 'z']
(Pdb) 

stepnext 之间的区别在于,step 将在调用的函数内停止,而 next 仅执行调用函数,然后在当前函数的下一行停止。当我们使用函数时,可以看到这种区别。

step 命令将在运行函数时迭代循环,显示循环的确切操作,首先使用 print(number) 打印一个数字,然后继续打印字母,返回到数字等:

(Pdb) step
> /Users/sammy/looping.py(5)<module>()
-> def nested_loop():
(Pdb) step
> /Users/sammy/looping.py(11)<module>()
-> if __name__ == '__main__':
(Pdb) step
> /Users/sammy/looping.py(12)<module>()
-> nested_loop()
(Pdb) step
--Call--
> /Users/sammy/looping.py(5)nested_loop()
-> def nested_loop():
(Pdb) step
> /Users/sammy/looping.py(6)nested_loop()
-> for number in num_list:
(Pdb) step
> /Users/sammy/looping.py(7)nested_loop()
-> print(number)
(Pdb) step
500
> /Users/sammy/looping.py(8)nested_loop()
-> for letter in alpha_list:
(Pdb) step
> /Users/sammy/looping.py(9)nested_loop()
-> print(letter)
(Pdb) step
x
> /Users/sammy/looping.py(8)nested_loop()
-> for letter in alpha_list:
(Pdb) step
> /Users/sammy/looping.py(9)nested_loop()
-> print(letter)
(Pdb) step
y
> /Users/sammy/looping.py(8)nested_loop()
-> for letter in alpha_list:
(Pdb)

相反,next 命令将执行整个函数,而不显示逐步过程。让我们使用 exit 命令退出当前会话,然后重新开始调试器:

python -m pdb looping.py

现在我们可以使用 next 命令:

(Pdb) next
> /Users/sammy/looping.py(5)<module>()
-> def nested_loop():
(Pdb) next
> /Users/sammy/looping.py(11)<module>()
-> if __name__ == '__main__':
(Pdb) next
> /Users/sammy/looping.py(12)<module>()
-> nested_loop()
(Pdb) next
500
x
y
z
600
x
y
z
700
x
y
z
--Return--
> /Users/sammy/looping.py(12)<module>()->None
-> nested_loop()
(Pdb)  

在浏览代码时,您可能希望检查传递给变量的值,您可以使用 pp 命令,它将使用 pprint 模块对表达式的值进行漂亮打印:

(Pdb) pp num_list
[500, 600, 700]
(Pdb) 

pdb 中的大多数命令都有更短的别名。step 的短形式是 snext 的短形式是 nhelp 命令将列出可用的别名。您还可以通过在提示符处按 ENTER 键来调用您上次调用的命令。

断点

通常,您将处理比上面示例更大的程序,因此您可能希望查看特定函数或行,而不是浏览整个程序。通过使用 break 命令设置断点,您将运行程序直到指定的断点。

当您插入断点时,调试器会为其分配一个编号。分配给断点的数字是从数字 1 开始的连续整数,您可以在处理断点时引用这些数字。

可以按照 <program_file>:<line_number> 的语法在特定行号设置断点,如下所示:

(Pdb) break looping.py:5
Breakpoint 1 at /Users/sammy/looping.py:5
(Pdb)

键入 clear 然后 y 以删除所有当前断点。然后,您可以在定义函数的地方设置断点:

(Pdb) break looping.nested_loop
Breakpoint 1 at /Users/sammy/looping.py:5
(Pdb) 

要删除当前断点,请键入 clear 然后 y。您还可以设置条件:

(Pdb) break looping.py:7, number > 500
Breakpoint 1 at /Users/sammy/looping.py:7
(Pdb)     

现在,如果我们发出 continue 命令,当 number x 的值大于 500 时程序将中断(也就是说,在外部循环的第二次迭代中将其设置为 600 时):

(Pdb) continue
500
x
y
z
> /Users/sammy/looping.py(7)nested_loop()
-> print(number)
(Pdb) 

要查看当前设置为运行的断点列表,请使用不带任何参数的 break 命令。您将收到有关您设置的特定断点的信息:

(Pdb) break
Num Type         Disp Enb   Where
1   breakpoint   keep yes   at /Users/sammy/looping.py:7
	stop only if number > 500
	breakpoint already hit 2 times
(Pdb) 

我们还可以使用 disable 命令和断点编号来禁用断点。在此会话中,我们添加了另一个断点,然后禁用第一个断点:

(Pdb) break looping.py:11
Breakpoint 2 at /Users/sammy/looping.py:11
(Pdb) disable 1
Disabled breakpoint 1 at /Users/sammy/looping.py:7
(Pdb) break
Num Type         Disp Enb   Where
1   breakpoint   keep no    at /Users/sammy/looping.py:7
	stop only if number > 500
	breakpoint already hit 2 times
2   breakpoint   keep yes   at /Users/sammy/looping.py:11
(Pdb) 

要启用断点,请使用 enable 命令,要完全删除断点,请使用 clear 命令:

(Pdb) enable 1
Enabled breakpoint 1 at /Users/sammy/looping.py:7
(Pdb) clear 2
Deleted breakpoint 2 at /Users/sammy/looping.py:11
(Pdb) 

pdb 中的断点为您提供了很多控制功能。一些附加功能包括使用 ignore 命令在程序的当前迭代中忽略断点(如 ignore 1)、使用 commands 命令在断点处触发操作(如 command 1)以及使用 tbreak 命令创建临时断点,该断点在程序执行第一次到达该点时自动清除(例如,要在第 3 行设置临时断点,您可以输入 tbreak 3)。

pdb 集成到程序中

您可以通过导入 pdb 模块并在您希望会话开始的行之前添加 pdb.set_trace() 函数来触发调试会话。

在我们上面的示例程序中,我们将添加 import 语句和我们希望进入调试器的函数。对于我们的示例,让我们在嵌套循环之前添加它。

# 导入 pdb 模块
import pdb

num_list = [500, 600, 700]
alpha_list = ['x', 'y', 'z']


def nested_loop():
    for number in num_list:
        print(number)

        # 在此行触发调试器
        pdb.set_trace()
        for letter in alpha_list:
            print(letter)

if __name__ == '__main__':
    nested_loop()

通过将调试器添加到代码中,您无需以特殊方式启动程序或记住设置断点。

导入 pdb 模块并运行 pdb.set_trace() 函数,让您可以像往常一样开始程序并通过其执行运行调试器。## 修改程序执行流程

Python 调试器允许您使用 jump 命令在运行时更改程序的流程。这使您可以跳过某些代码的执行,或者可以让您返回运行代码。

我们将使用一个创建包含字符串 sammy = "sammy" 中的字母列表的小程序进行演示:

def print_sammy():
    sammy_list = []
    sammy = "sammy"
    for letter in sammy:
        sammy_list.append(letter)
        print(sammy_list)

if __name__ == "__main__":
    print_sammy()

如果我们像往常一样使用 python letter_list.py 命令运行程序,将会收到以下输出:

['s']
['s', 'a']
['s', 'a', 'm']
['s', 'a', 'm', 'm']
['s', 'a', 'm', 'm', 'y']

使用 Python 调试器,让我们展示如何通过 跳转 在第一个循环后改变执行。当我们这样做时,我们会注意到 for 循环被中断:

python -m pdb letter_list.py
> /Users/sammy/letter_list.py(1)<module>()
-> def print_sammy():
(Pdb) list
  1  ->	def print_sammy():
  2  	    sammy_list = []
  3  	    sammy = "sammy"
  4  	    for letter in sammy:
  5  	        sammy_list.append(letter)
  6  	        print(sammy_list)
  7  	
  8  	if __name__ == "__main__":
  9  	    print_sammy()
 10  	
 11  	
(Pdb) break 5
Breakpoint 1 at /Users/sammy/letter_list.py:5
(Pdb) continue
> /Users/sammy/letter_list.py(5)print_sammy()
-> sammy_list.append(letter)
(Pdb) pp letter
's'
(Pdb) continue
['s']
> /Users/sammy/letter_list.py(5)print_sammy()
-> sammy_list.append(letter)
(Pdb) jump 6
> /Users/sammy/letter_list.py(6)print_sammy()
-> print(sammy_list)
(Pdb) pp letter
'a'
(Pdb) disable 1
Disabled breakpoint 1 at /Users/sammy/letter_list.py:5
(Pdb) continue
['s']
['s', 'm']
['s', 'm', 'm']
['s', 'm', 'm', 'y']

上述调试会话在第 5 行设置了断点以阻止代码继续执行,然后通过 continue 继续执行代码(并且漂亮地打印了一些 letter 的值以显示发生了什么)。接下来,我们使用 jump 命令跳转到第 6 行。此时,变量 letter 被设置为字符串 'a',但我们跳过了将其添加到列表 sammy_list 的代码。然后,我们使用 disable 命令禁用断点,以便使用 continue 命令按照通常的方式继续执行,因此 'a' 永远不会被附加到 sammy_list

接下来,我们可以退出第一个会话,重新启动调试器,在程序内部跳回,重新运行已经执行过的语句。这次,我们将在调试器中再次运行 for 循环的第一次迭代:

> /Users/sammy/letter_list.py(1)<module>()
-> def print_sammy():
(Pdb) list
  1  ->	def print_sammy():
  2  	    sammy_list = []
  3  	    sammy = "sammy"
  4  	    for letter in sammy:
  5  	        sammy_list.append(letter)
  6  	        print(sammy_list)
  7  	
  8  	if __name__ == "__main__":
  9  	    print_sammy()
 10  	
 11  	
(Pdb) break 6
Breakpoint 1 at /Users/sammy/letter_list.py:6
(Pdb) continue
> /Users/sammy/letter_list.py(6)print_sammy()
-> print(sammy_list)
(Pdb) pp letter
's'
(Pdb) jump 5
> /Users/sammy/letter_list.py(5)print_sammy()
-> sammy_list.append(letter)
(Pdb) continue
> /Users/sammy/letter_list.py(6)print_sammy()
-> print(sammy_list)
(Pdb) pp letter
's'
(Pdb) disable 1
Disabled breakpoint 1 at /Users/sammy/letter_list.py:6
(Pdb) continue
['s', 's']
['s', 's', 'a']
['s', 's', 'a', 'm']
['s', 's', 'a', 'm', 'm']
['s', 's', 'a', 'm', 'm', 'y']

在上述调试会话中,我们在第 6 行添加了一个断点,然后在继续执行后跳回到第 5 行。我们一路漂亮地打印,以显示字符串 's' 被附加到列表 sammy_list 两次。然后,我们禁用了第 6 行的断点,并继续运行程序。输出显示 's' 被附加到 sammy_list 两次。

调试器会阻止某些跳转,特别是在未定义的流程控制语句中跳转。例如,您不能在参数定义之前跳转到函数内部,也不能在 try:except 语句的中间跳转。您也不能跳出 finally 块。

Python 调试器的 jump 语句允许您在调试程序时更改执行流程,以查看是否可以将流程控制修改为不同的目的,或者更好地理解代码中出现的问题。

常用 pdb 命令表

以下是一张有用的 pdb 命令表,包括它们的简写形式,可在使用 Python 调试器时记住。

命令简写形式功能
argsa打印当前函数的参数列表
breakb在程序执行中创建断点(需要参数)
continueccont继续程序执行
helph提供命令列表或指定命令的帮助
jumpj设置要执行的下一行
listl打印当前行周围的源代码
nextn继续执行,直到达到当前函数中的下一行或返回
steps执行当前行,停在第一个可能的位置
pppp漂亮地打印表达式的值
quitexitq中止程序
returnr继续执行,直到当前函数返回

您可以从 Python 调试器文档中了解更多关于命令和调试器的工作方式的信息。

结论

调试是任何软件开发项目的重要步骤。Python 调试器 pdb 实现了一个交互式调试环境,您可以在其中使用任何用 Python 编写的程序。

通过让您暂停程序、查看变量的值以及以离散的逐步方式执行程序,您可以更全面地了解程序的运行情况,并找到逻辑中存在的错误或解决已知问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张无忌打怪兽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值