使用 Pdb 调试 Python

举报
Yuchuan 发表于 2021/12/27 16:36:07 2021/12/27
【摘要】 我将带您了解 pdb 的一些常见用法。您可能希望将本教程添加为书签,以便稍后在您真正需要时快速参考。pdb 和其他调试器是必不可少的工具。当您需要调试器时,没有替代品。你真的需要它。 在本教程结束时,您将了解如何使用调试器查看应用程序中任何变量的状态。您还可以随时停止和恢复应用程序的执行流程,因此您可以准确了解每行代码如何影响其内部状态。

目录

调试应用程序有时可能是一项不受欢迎的活动。您在时间紧迫的情况下忙于工作,而您只想让它发挥作用。然而,在其他时候,您可能正在学习一种新的语言功能或尝试一种新的方法,并希望更深入地了解某些东西是如何工作的。

不管情况如何,调试代码都是必要的,因此在调试器中工作起来很舒服是个好主意。在本教程中,我将向您展示使用 pdb(Python 的交互式源代码调试器)的基础知识。

我将带您了解 pdb 的一些常见用法。您可能希望将本教程添加为书签,以便稍后在您真正需要时快速参考。pdb 和其他调试器是必不可少的工具。当您需要调试器时,没有替代品。你真的需要它。

在本教程结束时,您将了解如何使用调试器查看应用程序中任何变量的状态。您还可以随时停止和恢复应用程序的执行流程,因此您可以准确了解每行代码如何影响其内部状态。

这对于追踪难以发现的错误非常有用,并允许您更快、更可靠地修复错误代码。有时,单步执行 pdb 中的代码并查看值如何变化会让人大开眼界,并导致“啊哈”时刻,以及偶尔的“手掌”。

pdb 是 Python 标准库的一部分,因此它始终存在并可供使用。如果您需要在无法访问您熟悉的 GUI 调试器的环境中调试代码,这可以成为救命稻草。

本教程中的示例代码使用 Python 3.6。您可以在GitHub找到这些示例的源代码。

在本教程的末尾,有一个基本 pdb 命令的快速参考。

还有一个可打印的 pdb 命令参考,您可以在调试时用作备忘单:

入门:打印变量的值

在第一个示例中,我们将看看以最简单的形式使用 pdb:检查变量的值。

在要中断调试器的位置插入以下代码:

import pdb; pdb.set_trace()

当上面的行被执行时,Python 停止并等待你告诉它下一步做什么。你会看到一个(Pdb)提示。这意味着您现在在交互式调试器中暂停并且可以输入命令。

从 Python 3.7 开始,还有另一种进入 debugger 的方法PEP 553描述了内置函数breakpoint(),这使得进入调试器变得容易且一致:

breakpoint()

默认情况下,breakpoint()导入 pdb和调用pdb.set_trace(),如上所示。但是, usingbreakpoint()更灵活,允许您通过其 API 和环境变量的使用来控制调试行为PYTHONBREAKPOINT。例如,PYTHONBREAKPOINT=0在您的环境中设置将完全禁用breakpoint(),从而禁用调试。如果你正在使用Python 3.7或更高版本,我鼓励你使用breakpoint()的不是pdb.set_trace()

您还可以通过直接从命令行运行 Python 并传递选项来闯入调试器,而无需修改源代码和使用pdb.set_trace()或。如果您的应用程序接受命令行参数,请像往常一样在文件名之后传递它们。例如:breakpoint()-m pdb

$ python3 -m pdb app.py arg1 arg2

有很多 pdb 命令可用。在本教程的末尾,有一个基本 pdb 命令列表。现在,让我们使用该p命令来打印变量的值。p variable_name(Pdb)提示符下输入以打印其值。

让我们看一下这个例子。这是example1.py来源:

#!/usr/bin/env python3

filename = __file__
import pdb; pdb.set_trace()
print(f'path = {filename}')

如果你从你的 shell 运行它,你应该得到以下输出:

$ ./example1.py 
> /code/example1.py(5)<module>()
-> print(f'path = {filename}')
(Pdb) 

如果您在从命令行获取示例或您自己的代码时遇到问题,请阅读如何使用 Python 创建自己的命令行命令?如果您使用的是 Windows,请查看Python Windows 常见问题解答

现在输入p filename。你应该看到:

(Pdb) p filename
'./example1.py'
(Pdb)

由于您在 shell 中并使用 CLI(命令行界面),因此请注意字符和格式。他们会给你你需要的上下文:

  • >从第一行开始,告诉您您所在的源文件。在文件名之后,括号中是当前行号。接下来是函数的名称。在这个例子中,由于我们没有在函数内部和模块级别暂停,我们看到<module>().
  • ->开始第二行,是 Python 暂停的当前源代码行。该行尚未执行。在本例中,这是来自上一行的line 5in 。example1.py>
  • (Pdb)是 pdb 的提示。它在等待命令。

使用该命令q退出调试并退出。

打印表达式

使用 print 命令时p,您将传递一个要由 Python 计算的表达式。如果传递变量名,pdb 将打印其当前值。但是,您可以做更多的事情来调查正在运行的应用程序的状态。

在这个例子中,函数get_path()被调用。为了检查这个函数中发生了什么,我pdb.set_trace()在它返回之前插入了一个暂停执行的调用:

#!/usr/bin/env python3

import os


def get_path(filename):
    """Return file's path or empty string if no path."""
    head, tail = os.path.split(filename)
    import pdb; pdb.set_trace()
    return head


filename = __file__
print(f'path = {get_path(filename)}')

如果你从你的 shell 运行它,你应该得到输出:

$ ./example2.py 
> /code/example2.py(10)get_path()
-> return head
(Pdb) 

我们在哪?

  • >:我们在源文件中example2.py上线10了函数get_path()。这是p命令将用于解析变量名称的参考框架,即当前作用域或上下文。
  • ->: 执行已暂停return head。该行尚未执行。这是行10example2.py在函数get_path(),从>上面的行。

让我们打印一些表达式来查看应用程序的当前状态。我ll最初使用命令(longlist) 来列出函数的源代码:

(Pdb) ll
  6     def get_path(filename):
  7         """Return file's path or empty string if no path."""
  8         head, tail = os.path.split(filename)
  9         import pdb; pdb.set_trace()
 10  ->     return head
(Pdb) p filename
'./example2.py'
(Pdb) p head, tail
('.', 'example2.py')
(Pdb) p 'filename: ' + filename
'filename: ./example2.py'
(Pdb) p get_path
<function get_path at 0x100760e18>
(Pdb) p getattr(get_path, '__doc__')
"Return file's path or empty string if no path."
(Pdb) p [os.path.split(p)[1] for p in os.path.sys.path]
['pdb-basics', 'python36.zip', 'python3.6', 'lib-dynload', 'site-packages']
(Pdb) 

您可以将任何有效的 Python 表达式传递给以p进行评估。

当您正在调试并希望在运行时直接在应用程序中测试替代实现时,这尤其有用。

您还可以使用命令pp(pretty-print) 来漂亮地打印表达式。如果您想打印具有大量输出(例如列表和字典)的变量或表达式,这将很有帮助。如果可以的话,漂亮打印将对象保留在一行上,如果它们不适合允许的宽度,则将它们分成多行。

单步执行代码

在调试时,您可以使用两个命令来单步调试代码:

命令 描述
n (next) 继续执行,直到到达当前函数的下一行或返回。
s (step) 执行当前行并在第一个可能的情况下停止(在被调用的函数中或在当前函数中)。

有一个名为unt(until)的第三个命令。它与n(下)有关。我们将在本教程后面的继续执行部分中查看它。

n(next) 和s(step)之间的区别在于pdb 停止的位置。

使用n(next) 继续执行直到下一行并停留在当前函数中,即如果调用了一个外部函数,则不会停止。将下一步视为“留在本地”或“跨步”。

使用s(step) 执行当前行并在调用外部函数时停止。将步骤视为“步入”。如果执行在另一个函数中停止,s将打印--Call--.

双方ns达到当前函数结束时将停止执行并打印--Return--与之后的下一行的末尾的返回值一起->

让我们看一个使用这两个命令的示例。这是example3.py来源:

#!/usr/bin/env python3

import os


def get_path(filename):
    """Return file's path or empty string if no path."""
    head, tail = os.path.split(filename)
    return head


filename = __file__
import pdb; pdb.set_trace()
filename_path = get_path(filename)
print(f'path = {filename_path}')

如果你从你的 shell 运行它并输入n,你应该得到输出:

$ ./example3.py 
> /code/example3.py(14)<module>()
-> filename_path = get_path(filename)
(Pdb) n
> /code/example3.py(15)<module>()
-> print(f'path = {filename_path}')
(Pdb) 

随着n(next),我们停在 line 15,下一行。我们“留在本地”<module>()并“跳过”对get_path(). 该函数是<module>()因为我们当前处于模块级别并且没有在另一个函数中暂停。

让我们试试s

$ ./example3.py 
> /code/example3.py(14)<module>()
-> filename_path = get_path(filename)
(Pdb) s
--Call--
> /code/example3.py(6)get_path()
-> def get_path(filename):
(Pdb) 

使用s(step),我们6在函数中在线停止,get_path()因为它是在线调用的14。注意命令--Call--后面的行s

方便的是,pdb 会记住您的上一个命令。如果您要单步执行大量代码,只需按Enter重复最后一个命令即可。

下面是使用s和 单n步执行代码的示例。我s最初进入是因为我想“步入”该功能get_path()并停止。然后我输入n一次以“保持本地”或“跳过”任何其他函数调用,然后按Enter重复该n命令,直到到达最后一个源代码行。

$ ./example3.py 
> /code/example3.py(14)<module>()
-> filename_path = get_path(filename)
(Pdb) s
--Call--
> /code/example3.py(6)get_path()
-> def get_path(filename):
(Pdb) n
> /code/example3.py(8)get_path()
-> head, tail = os.path.split(filename)
(Pdb) 
> /code/example3.py(9)get_path()
-> return head
(Pdb) 
--Return--
> /code/example3.py(9)get_path()->'.'
-> return head
(Pdb) 
> /code/example3.py(15)<module>()
-> print(f'path = {filename_path}')
(Pdb) 
path = .
--Return--
> /code/example3.py(15)<module>()->None
-> print(f'path = {filename_path}')
(Pdb) 

注意行--Call----Return--。这是 pdb,让您知道执行停止的原因。n(next) 和s(step) 将在函数返回之前停止。这就是为什么你会看到--Return--上面的线条。

还要注意上面->'.'第一个之后的行尾--Return--

--Return--
> /code/example3.py(9)get_path()->'.'
-> return head
(Pdb) 

当 pdb 在函数返回之前停止在函数末尾时,它还会为您打印返回值。在这个例子中,它是'.'.

列出源代码

不要忘记命令ll(长列表:列出当前函数或框架的整个源代码)。当您单步执行不熟悉的代码或只想查看整个函数的上下文时,这真的很有帮助。

下面是一个例子:

$ ./example3.py 
> /code/example3.py(14)<module>()
-> filename_path = get_path(filename)
(Pdb) s
--Call--
> /code/example3.py(6)get_path()
-> def get_path(filename):
(Pdb) ll
  6  -> def get_path(filename):
  7         """Return file's path or empty string if no path."""
  8         head, tail = os.path.split(filename)
  9         return head
(Pdb) 

要查看更短的代码片段,请使用命令l(list)。如果没有参数,它将在当前行周围打印 11 行或继续上一个列表。传递参数.以始终在当前行周围列出 11 行:l .

$ ./example3.py 
> /code/example3.py(14)<module>()
-> filename_path = get_path(filename)
(Pdb) l
  9         return head
 10     
 11     
 12     filename = __file__
 13     import pdb; pdb.set_trace()
 14  -> filename_path = get_path(filename)
 15     print(f'path = {filename_path}')
[EOF]
(Pdb) l
[EOF]
(Pdb) l .
  9         return head
 10     
 11     
 12     filename = __file__
 13     import pdb; pdb.set_trace()
 14  -> filename_path = get_path(filename)
 15     print(f'path = {filename_path}')
[EOF]
(Pdb) 

使用断点

断点非常方便,可以为您节省大量时间。无需单步执行您不感兴趣的数十行,只需在要调查的位置创建一个断点即可。或者,您还可以告诉 pdb 仅在特定条件为真时中断。

使用命令b(break) 设置断点。您可以指定停止执行的行号或函数名称。

中断的语法是:

b(reak) [ ([filename:]lineno | function) [, condition] ]

如果filename:未在行号之前指定lineno,则使用当前源文件。

请注意,可选的第二个参数bcondition。这是非常强大的。想象一下这样一种情况,您仅在特定条件存在时才想中断。如果您将 Python 表达式作为第二个参数传递,则当表达式的计算结果为 true 时 pdb 将中断。我们将在下面的示例中执行此操作。

在这个例子中,有一个实用程序模块util.py。让我们在函数中设置一个断点来停止执行get_path()

这是主脚本的来源example4.py

#!/usr/bin/env python3

import util

filename = __file__
import pdb; pdb.set_trace()
filename_path = util.get_path(filename)
print(f'path = {filename_path}')

这是实用程序模块的来源util.py

def get_path(filename):
    """Return file's path or empty string if no path."""
    import os
    head, tail = os.path.split(filename)
    return head

首先,让我们使用源文件名和行号设置断点:

$ ./example4.py 
> /code/example4.py(7)<module>()
-> filename_path = util.get_path(filename)
(Pdb) b util:5
Breakpoint 1 at /code/util.py:5
(Pdb) c
> /code/util.py(5)get_path()
-> return head
(Pdb) p filename, head, tail
('./example4.py', '.', 'example4.py')
(Pdb) 

命令c(continue) 继续执行,直到找到断点。

接下来,让我们使用函数名称设置断点:

$ ./example4.py 
> /code/example4.py(7)<module>()
-> filename_path = util.get_path(filename)
(Pdb) b util.get_path
Breakpoint 1 at /code/util.py:1
(Pdb) c
> /code/util.py(3)get_path()
-> import os
(Pdb) p filename
'./example4.py'
(Pdb) 

b不带参数输入以查看所有断点的列表:

(Pdb) b
Num Type         Disp Enb   Where
1   breakpoint   keep yes   at /code/util.py:1
(Pdb) 

您可以使用命令disable bpnumber和禁用和重新启用断点enable bpnumberbpnumber是断点列表第一列中的断点编号Num。注意Enb列的值变化:

(Pdb) disable 1
Disabled breakpoint 1 at /code/util.py:1
(Pdb) b
Num Type         Disp Enb   Where
1   breakpoint   keep no    at /code/util.py:1
(Pdb) enable 1
Enabled breakpoint 1 at /code/util.py:1
(Pdb) b
Num Type         Disp Enb   Where
1   breakpoint   keep yes   at /code/util.py:1
(Pdb) 

要删除断点,请使用命令cl(clear):

cl(ear) filename:lineno
cl(ear) [bpnumber [bpnumber...]]

现在让我们使用 Python 表达式来设置断点。想象一下这样一种情况:只有当您的问题函数收到某个输入时,您才想中断。

在此示例场景中,get_path()函数在收到相对路径时失败,即文件的路径不以/. 在这种情况下,我将创建一个计算结果为 true 的表达式,并将其b作为第二个参数传递给:

$ ./example4.py 
> /code/example4.py(7)<module>()
-> filename_path = util.get_path(filename)
(Pdb) b util.get_path, not filename.startswith('/')
Breakpoint 1 at /code/util.py:1
(Pdb) c
> /code/util.py(3)get_path()
-> import os
(Pdb) a
filename = './example4.py'
(Pdb) 

创建上面的断点并输入c继续执行后,当表达式计算为真时,pdb 停止。命令a(args) 打印当前函数的参数列表。

在上面的示例中,当您使用函数名称而不是行号设置断点时,请注意表达式应仅使用函数参数或在输入函数时可用的全局变量。否则,断点将停止在函数中执行,而不管表达式的值如何。

如果您需要使用变量名位于函数内的表达式来中断,即变量名不在函数的参数列表中,请指定行号:

$ ./example4.py 
> /code/example4.py(7)<module>()
-> filename_path = util.get_path(filename)
(Pdb) b util:5, not head.startswith('/')
Breakpoint 1 at /code/util.py:5
(Pdb) c
> /code/util.py(5)get_path()
-> return head
(Pdb) p head
'.'
(Pdb) a
filename = './example4.py'
(Pdb) 

您还可以使用命令设置临时断点tbreak。当它第一次被击中时它会自动移除。它使用与b.

继续执行

到目前为止,我们已经研究了使用n(next) 和s(step) 单步执行代码以及使用b(break) 和c(continue)使用断点。

还有一个相关的命令:(unt直到)。

用于unt像 一样继续执行c,但在比当前行大的下一行停止。有时unt使用起来更方便、更快捷,这正是您想要的。我将通过下面的示例来演示这一点。

我们先来看看语法和描述unt

命令 句法 描述
unt unt (il) [lineno] 如果没有lineno,则继续执行,直到到达编号大于当前编号的行。使用lineno,继续执行,直到到达数字大于或等于该数字的行。在这两种情况下,也在当前帧返回时停止。

根据您是否传递行号参数linenount可以以两种方式运行:

  • 如果没有lineno,则继续执行,直到到达编号大于当前编号的行。这类似于n(下一个)。这是执行和“跳过”代码的另一种方式。之间的差nuntunt达到与一个大于当前一个的线,只有当停止。n将在下一个逻辑执行的行处停止。
  • 使用lineno,继续执行,直到到达数字大于或等于该数字的行。这就像c(继续)带有行号参数。

在这两种情况下,unt在当前帧(函数)返回时停止,就像n(next) 和s(step) 一样。

需要注意的主要行为unt是,当达到大于或等于当前或指定行的行号时,它将停止。

unt当您想继续执行并在当前源文件中更远的地方停止时使用。您可以将其视为n(next) 和b(break)的混合体,具体取决于您是否传递行号参数。

在下面的示例中,有一个带有循环的函数。在这里,您希望继续执行代码并在循环后停止,而不是单步执行循环的每次迭代或设置断点:

这是示例源example4unt.py

#!/usr/bin/env python3

import os


def get_path(fname):
    """Return file's path or empty string if no path."""
    import pdb; pdb.set_trace()
    head, tail = os.path.split(fname)
    for char in tail:
        pass  # Check filename char
    return head


filename = __file__
filename_path = get_path(filename)
print(f'path = {filename_path}')

和控制台输出使用unt

$ ./example4unt.py 
> /code/example4unt.py(9)get_path()
-> head, tail = os.path.split(fname)
(Pdb) ll
  6     def get_path(fname):
  7         """Return file's path or empty string if no path."""
  8         import pdb; pdb.set_trace()
  9  ->     head, tail = os.path.split(fname)
 10         for char in tail:
 11             pass  # Check filename char
 12         return head
(Pdb) unt
> /code/example4unt.py(10)get_path()
-> for char in tail:
(Pdb) 
> /code/example4unt.py(11)get_path()
-> pass  # Check filename char
(Pdb) 
> /code/example4unt.py(12)get_path()
-> return head
(Pdb) p char, tail
('y', 'example4unt.py')

ll命令首先用于打印函数的源代码,然后是unt. pdb 会记住上次输入的命令,所以我只需按下Enter即可重复该unt命令。这将继续执行代码,直到到达比当前行大的源代码行。

请注意,在上面的控制台输出中,pdb 仅在行1011. 由于unt已使用,因此仅在循环的第一次迭代中停止执行。但是,循环的每次迭代都被执行。这可以在输出的最后一行进行验证。该char变量的值'y'等于在最后一个字符tail的值'example4unt.py'

显示表达式

与使用pand打印表达式类似pp,您可以使用该命令display [expression]告诉 pdb 在执行停止时自动显示表达式的值(如果它发生更改)。使用该命令undisplay [expression]清除显示表达式。

以下是这两个命令的语法和说明:

命令 句法 描述
display 显示 [表达] 显示值expression如果改变了,每次在当前帧停止执行。如果没有expression,则列出当前帧的所有显示表达式。
undisplay 取消显示 [表达式] expression在当前帧中不再显示。如果没有expression,则清除当前帧的所有显示表达式。

下面是一个示例,example4display.py演示了它在循环中的使用:

$ ./example4display.py 
> /code/example4display.py(9)get_path()
-> head, tail = os.path.split(fname)
(Pdb) ll
  6     def get_path(fname):
  7         """Return file's path or empty string if no path."""
  8         import pdb; pdb.set_trace()
  9  ->     head, tail = os.path.split(fname)
 10         for char in tail:
 11             pass  # Check filename char
 12         return head
(Pdb) b 11
Breakpoint 1 at /code/example4display.py:11
(Pdb) c
> /code/example4display.py(11)get_path()
-> pass  # Check filename char
(Pdb) display char
display char: 'e'
(Pdb) c
> /code/example4display.py(11)get_path()
-> pass  # Check filename char
display char: 'x'  [old: 'e']
(Pdb) 
> /code/example4display.py(11)get_path()
-> pass  # Check filename char
display char: 'a'  [old: 'x']
(Pdb) 
> /code/example4display.py(11)get_path()
-> pass  # Check filename char
display char: 'm'  [old: 'a']

在上面的输出中,pdb 会自动显示char变量的值,因为每次遇到断点时,它的值都会发生变化。有时这很有帮助并且正是您想要的,但是还有另一种使用display.

您可以display多次输入以构建表达式监视列表。这比p. 添加您感兴趣的所有表达式后,只需输入display即可查看当前值:

$ ./example4display.py 
> /code/example4display.py(9)get_path()
-> head, tail = os.path.split(fname)
(Pdb) ll
  6     def get_path(fname):
  7         """Return file's path or empty string if no path."""
  8         import pdb; pdb.set_trace()
  9  ->     head, tail = os.path.split(fname)
 10         for char in tail:
 11             pass  # Check filename char
 12         return head
(Pdb) b 11
Breakpoint 1 at /code/example4display.py:11
(Pdb) c
> /code/example4display.py(11)get_path()
-> pass  # Check filename char
(Pdb) display char
display char: 'e'
(Pdb) display fname
display fname: './example4display.py'
(Pdb) display head
display head: '.'
(Pdb) display tail
display tail: 'example4display.py'
(Pdb) c
> /code/example4display.py(11)get_path()
-> pass  # Check filename char
display char: 'x'  [old: 'e']
(Pdb) display
Currently displaying:
char: 'x'
fname: './example4display.py'
head: '.'
tail: 'example4display.py'

Python Caller ID

在最后一节中,我们将建立在我们迄今为止所学的基础上,并以不错的回报结束。我使用名称“来电显示”来指代电话系统的来电显示功能。这正是本示例演示的内容,只是它应用于 Python。

这是主脚本的来源example5.py

#!/usr/bin/env python3

import fileutil


def get_file_info(full_fname):
    file_path = fileutil.get_path(full_fname)
    return file_path


filename = __file__
filename_path = get_file_info(filename)
print(f'path = {filename_path}')

这是实用程序模块fileutil.py

def get_path(fname):
    """Return file's path or empty string if no path."""
    import os
    import pdb; pdb.set_trace()
    head, tail = os.path.split(fname)
    return head

在这种情况下,假设有一个大型代码库,其中包含一个实用程序模块中的函数,get_path()使用无效输入调用该函数。但是,它是从不同包中的许多地方调用的。

How do you find who the caller is?

使用命令w(where) 打印堆栈跟踪,最新的帧位于底部:

$ ./example5.py 
> /code/fileutil.py(5)get_path()
-> head, tail = os.path.split(fname)
(Pdb) w
  /code/example5.py(12)<module>()
-> filename_path = get_file_info(filename)
  /code/example5.py(7)get_file_info()
-> file_path = fileutil.get_path(full_fname)
> /code/fileutil.py(5)get_path()
-> head, tail = os.path.split(fname)
(Pdb) 

如果这看起来令人困惑,或者您不确定堆栈跟踪或框架是什么,请不要担心。我将在下面解释这些术语。这并不像听起来那么困难。

由于最近的帧在底部,从那里开始并从下往上阅读。查看以 开头的行->,但跳过第一个实例,因为它pdb.set_trace()是用于在函数中输入 pdb 的位置get_path()。在此示例中,调用该函数的源代码行get_path()是:

-> file_path = fileutil.get_path(full_fname)

每个上面的行->包含文件名、行号(在括号中)和源行所在的函数名。所以调用者是:

  /code/example5.py(7)get_file_info()
-> file_path = fileutil.get_path(full_fname)

在这个用于演示目的的小示例中,这并不奇怪,但想象一下一个大型应用程序,您在其中设置了一个带有条件的断点,以确定错误输入值的来源。

现在我们知道如何找到调用者了。

但是这个堆栈跟踪和框架的东西呢?

一个堆栈跟踪只是所有的Python已经创建的跟踪函数调用的帧列表。框架是 Python 在调用函数时创建并在函数返回时删除的数据结构。堆栈只是在任何时间点的帧或函数调用的有序列表。(函数调用)堆栈在应用程序的整个生命周期中随着函数被调用然后返回而增长和缩小。

打印时,这个有序的帧列表,即堆栈,称为堆栈跟踪。您可以随时通过输入命令来查看它w,就像我们在上面找到调用者一样。

有关详细信息,请参阅维基百科上的调用堆栈文章

为了更好地理解并充分利用 pdb,让我们更仔细地查看以下帮助w

(Pdb) h w
w(here)
        Print a stack trace, with the most recent frame at the bottom.
        An arrow indicates the "current frame", which determines the
        context of most commands. 'bt' is an alias for this command.

pdb 的“当前帧”是什么意思?

将当前帧视为 pdb 已停止执行的当前函数。换句话说,当前帧是您的应用程序当前暂停的位置,并用作 pdb 命令p(如(print))的“参考帧” 。

p和其他命令将在需要时使用当前帧作为上下文。在 的情况下p,当前帧将用于查找和打印变量引用。

当 pdb 打印堆栈跟踪时,箭头>指示当前帧。

这有什么用?

您可以使用u(向上)和d(向下)两个命令来更改当前帧。结合p,这允许您在任何帧中调用堆栈的任何点检查应用程序中的变量和状态。

以下是这两个命令的语法和说明:

命令 句法 描述
u u(p) [count] 将当前帧count(默认一帧)在堆栈跟踪中向上移动(到较旧的帧)。
d d(own) [count] count堆栈跟踪中的当前帧(默认一帧)向下移动(到较新的帧)。

让我们看一个使用uandd命令的例子。在这种情况下,我们要检查变量full_fname局部于功能get_file_info()example5.py。为此,我们必须使用以下命令将当前帧向上更改一级u

$ ./example5.py 
> /code/fileutil.py(5)get_path()
-> head, tail = os.path.split(fname)
(Pdb) w
  /code/example5.py(12)<module>()
-> filename_path = get_file_info(filename)
  /code/example5.py(7)get_file_info()
-> file_path = fileutil.get_path(full_fname)
> /code/fileutil.py(5)get_path()
-> head, tail = os.path.split(fname)
(Pdb) u
> /code/example5.py(7)get_file_info()
-> file_path = fileutil.get_path(full_fname)
(Pdb) p full_fname
'./example5.py'
(Pdb) d
> /code/fileutil.py(5)get_path()
-> head, tail = os.path.split(fname)
(Pdb) p fname
'./example5.py'
(Pdb) 

该呼叫pdb.set_trace()是在fileutil.py该函数get_path(),因此当前帧的初始设置那里。您可以在上面的第一行输出中看到它:

> /code/fileutil.py(5)get_path()

为了访问和打印full_fname函数get_file_info()中的局部变量example5.py,该命令u用于向上移动一级:

(Pdb) u
> /code/example5.py(7)get_file_info()
-> file_path = fileutil.get_path(full_fname)

请注意,在u上面的输出中,pdb>在第一行的开头打印了箭头。这是 pdb,让您知道框架已更改,并且此源位置现在是当前框架。full_fname现在可以访问该变量。此外,重要的是要意识到从->第 2 行开始的源代码行已被执行。由于帧被向上移动到堆栈中,fileutil.get_path()因此被调用。使用u,我们搬到了堆栈(从某种意义上说,早在时间)的功能example5.get_file_info()在那里fileutil.get_path()被调用。

继续这个例子,在full_fname被打印之后,当前帧使用 移动到它的原始位置d,并且局部变量fnameinget_path()被打印。

如果我们愿意,我们可以通过将count参数传递给uor来一次移动多个帧d。例如,我们可以example5.py通过输入u 2以下内容进入模块级别:

$ ./example5.py 
> /code/fileutil.py(5)get_path()
-> head, tail = os.path.split(fname)
(Pdb) u 2
> /code/example5.py(12)<module>()
-> filename_path = get_file_info(filename)
(Pdb) p filename
'./example5.py'
(Pdb) 

当您进行调试和思考许多不同的事情时,很容易忘记您身在何处。请记住,您始终可以使用恰当命名的命令w(where) 来查看执行暂停的位置以及当前帧是什么。

基本的 pdb 命令

一旦您花一点时间使用 pdb,您就会意识到一点点知识大有帮助。该h命令始终提供帮助。

只需输入hhelp <topic>即可获取所有命令的列表或特定命令或主题的帮助。

为了快速参考,以下是基本命令列表:

命令 描述
p 打印表达式的值。
pp 漂亮地打印表达式的值。
n 继续执行,直到到达当前函数的下一行或返回。
s 执行当前行并在第一个可能的情况下停止(在被调用的函数中或在当前函数中)。
c 继续执行,仅在遇到断点时停止。
unt 继续执行,直到到达编号大于当前编号的行。使用行号参数,继续执行,直到到达数字大于或等于该行的行。
l 列出当前文件的源代码。在没有参数的情况下,在当前行周围列出 11 行或继续上一个列表。
ll 列出当前函数或框架的全部源代码。
b 没有参数,列出所有中断。使用行号参数,在当前文件的这一行设置一个断点。
w 打印堆栈跟踪,最新的帧在底部。箭头指示当前帧,它决定了大多数命令的上下文。
u 将当前帧计数(默认为一)在堆栈跟踪中向上移动(到较旧的帧)。
d 在堆栈跟踪中向下移动当前帧计数(默认为一个)级别(到更新的帧)。
h 查看可用命令列表。
h <topic> 显示命令或主题的帮助。
h pdb 显示完整的 pdb 文档。
q 退出调试器并退出。

使用 pdb 调试 Python:结论

在本教程中,我们介绍了 pdb 的一些基本和常见用法:

  • 打印表达式
  • 使用n(next) 和s(step) 单步执行代码
  • 使用断点
  • 继续执行unt(直到)
  • 显示表达式
  • 查找函数的调用者

我希望它对你有帮助。如果您想了解更多信息,请参阅:

示例中使用的源代码可以在相关的GitHub 存储库找到。请务必查看我们的可打印 pdb 命令参考,您可以在调试时将其用作备忘单:

此外,如果您想尝试基于 GUI 的 Python 调试器,请阅读我们的Python IDE 和编辑器指南,了解哪些选项最适合您。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。