Skip to content

Latest commit

 

History

History
483 lines (344 loc) · 15.9 KB

File metadata and controls

483 lines (344 loc) · 15.9 KB

十一、将 PDB 用于调试

即使有一个全面的自动化测试套件,我们仍然可以进入需要调试器来找出发生了什么的情况。幸运的是,Python 包含了一个带有标准库的强大调试器:PDB。PDB 是一个命令行调试器,如果您熟悉 GDB 之类的工具,那么您已经对如何使用 PDB 有了很好的了解。

与其他 Python 调试器相比,PDB 的关键优势在于,作为 Python 本身的一部分,PDB 几乎可以在 Python 所在的任何地方使用,包括 Python 语言嵌入到更大系统中的专用环境,如 ESRI 的ArcGIS地理信息系统。这就是说,它可以更舒适地使用

所谓的图形调试器,如Jetbrains’**PyCharm微软针对 Visual Studio 的Python 工具等产品附带的调试器。您可以跳过本章,直到熟悉 PDB 变得更加迫切;在本书的后面部分,或者在Python 熟练工Python 大师中,您不会错过我们所依赖的任何内容。

PDB 不同于许多调试工具,因为它实际上不是一个单独的程序,而是一个模块,就像任何其他 Python 模块一样。您可以将pdb导入任何程序,并使用set_trace()函数调用启动调试器。此函数只需在程序执行过程中的任何点启动调试器。

在我们第一次查看 PDB 时,让我们使用 REPL 并使用set_trace()启动调试器:

>>> import pdb
>>> pdb.set_trace()
--Return--
> <stdin>(1)<module>()->None
(Pdb)

执行set_trace()后,您将看到提示从三重 V 形变为(Pdb)–这就是您知道自己在调试器中的方式。

调试命令

我们要做的第一件事就是通过键入 help 来查看调试器中有哪些可用命令:

(Pdb) help

Documented commands (type help <topic>):
========================================
EOF    cl         disable  interact  next     return  u          where
a      clear      display  j         p        retval  unalias
alias  commands   down     jump      pp       run     undisplay
args   condition  enable   l         print    rv      unt
b      cont       exit     list      q        s       until
break  continue   h        ll        quit     source  up
bt     d          help     longlist  r        step    w
c      debug      ignore   n         restart  tbreak  whatis

Miscellaneous help topics:
==========================
pdb  exec

这里列出了几十个命令,其中一些命令几乎在每个调试会话中都会使用,而有些命令可能永远不会使用。

通过键入 help,然后键入命令名,可以获得有关命令的特定帮助。例如,要查看 continue 的作用,请键入help continue

(Pdb) help continue
c(ont(inue))
Continue execution, only stop when a breakpoint is encountered.

命令名中奇怪的括号告诉您,continue可以通过键入ccont或完整的单词 continue 来激活 。了解常用 PDB 命令的快捷方式可以大大提高调试的舒适性和速度。

回文调试

我们将调试一个简单的函数,而不是简单地列出所有常用的 PDB 命令。我们的函数–is_palindrome()–接收一个整数,并确定该整数的数字是否为回文。回文是前后相同的序列。

我们要做的第一件事是创建一个新文件,palindrome.py,其中包含以下代码:

import unittest

def digits(x):
 """Convert an integer into a list of digits.

 Args:
 x: The number whose digits we want.

 Returns: A list of the digits, in order, of ``x``.

 >>> digits(4586378)
 [4, 5, 8, 6, 3, 7, 8]
 """

 digs = []
 while x != 0:
 div, mod = divmod(x, 10)
 digs.append(mod)
 x = mod
 return digs

def is_palindrome(x):
 """Determine if an integer is a palindrome.

 Args:
 x: The number to check for palindromicity.

 Returns: True if the digits of ``x`` are a palindrome,
 False otherwise.

 >>> is_palindrome(1234)
 False
 >>> is_palindrome(2468642)
 True
 """
 digs = digits(x)
 for f, r in zip(digs, reversed(digs)):
 if f != r:
 return False
 return True

class Tests(unittest.TestCase):
 """Tests for the ``is_palindrome()`` function."""
 def test_negative(self):
 "Check that it returns False correctly."
 self.assertFalse(is_palindrome(1234))

 def test_positive(self):
 "Check that it returns True correctly."
 self.assertTrue(is_palindrome(1234321))

 def test_single_digit(self):
 "Check that it works for single digit numbers."
 for i in range(10):
 self.assertTrue(is_palindrome(i))

if __name__ == '__main__':
 unittest.main()

如您所见,我们的代码有三个主要部分:

  • 第一个是digits()函数,它将整数转换为数字列表。
  • 第二个是is_palindrome()函数,它首先调用digits(),然后检查生成的列表是否是回文。
  • 第三部分是一组单元测试。我们将使用这些测试来驱动程序。

正如您所料,这是关于调试的一节,代码中有一个 bug。我们将首先运行程序并注意到 bug,然后我们将看到如何使用 PDB 来查找 bug。

用 PDB 搜索 Bug

那么,让我们简单地运行这个程序。我们希望运行三个测试,由于这是一个相对简单的程序,我们希望它运行得非常快:

$ python palindrome.py

我们看到的不是快速运行,而是这个程序似乎永远运行!如果你看一下它的内存使用情况,你也会发现它运行的时间越长,它的大小就越大。显然出了问题,所以让我们使用Ctrl+C来终止程序。

让我们使用 PDB 来了解这里发生了什么。因为我们不知道我们的问题可能在哪里,所以我们不知道在哪里打电话。因此,我们将使用命令行调用在 PDB 的控制下启动程序:

$ python -m pdb palindrome.py
> /Users/sixty_north/examples/palindrome.py(1)<module>()
-> import unittest
(Pdb)

这里我们使用-m 参数,它告诉 Python 以脚本的形式执行特定的模块——在本例中是 PDB。其余参数将传递给该脚本。因此,这里我们告诉 Python 以脚本的形式执行 PDB 模块,并将中断文件的名称传递给它。

我们看到的是,我们立即被带到 PDB 提示符。指向import unittest的箭头告诉我们,这是我们继续时将执行的下一条语句。但这句话在哪里?

让我们使用 where 命令来查找:

(Pdb) where
 /Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/bdb.py(387)run()
-> exec cmd in globals, locals
 <string>(1)<module>()
> /Users/sixty_north/examples/palindrome.py(1)<module>()
-> import unittest

where 命令报告我们当前的调用堆栈,最新的帧在底部,我们可以看到 PDB 已经在palindrome.py的第一行暂停了执行。这加强了我们之前讨论过的 Python 执行的一个重要方面:在运行时对所有内容进行评估。在本例中,我们在 import 语句之前暂停了执行。

我们可以通过使用 next 命令运行到下一条语句来执行此导入:

(Pdb) next
> /Users/sixty_north/examples/palindrome.py(3)<module>()
-> def digits(x):
(Pdb)

我们看到这将带我们进入对digits()函数的def调用。当我们执行另一个 next 时,我们转到is_palindrome()函数的定义:

(Pdb) next
> /Users/sixty_north/examples/palindrome.py(12)<module>()
-> def is_palindrome(x):
(Pdb)

You may be wondering why the debugger didn't step into the body of digits. After all, isn't it evaluated at runtime like everything else? The answer is that the body of the function can only be evaluated when there are arguments supplied to it, so it will be run only when the function is called. The bodies of functions are checked for proper syntax when they're imported, but PDB doesn't let us debug that part of the process.

用采样法寻找无限循环

我们可以继续使用 next 来完成程序的执行,但是由于我们不知道错误在哪里,这可能不是一种非常有用的技术。相反,请记住,我们的程序的问题在于它似乎永远在运行。这听起来很像一个无限循环!

因此,我们不需要单步执行我们的代码,只需让它执行,然后在我们认为可能处于该循环时,使用Ctrl+C重新进入调试器:

(Pdb) cont
^C
Program interrupted. (Use 'cont' to resume).
> /Users/sixty_north/examples/palindrome.py(9)digits()
-> x = mod
(Pdb)

让程序运行几秒钟后,我们按Ctrl+C停止程序,显示我们处于palindrome.pydigits()功能。如果我们想在该行查看源代码,可以使用 PDB 命令列表:

(Pdb) list
 4       "Convert an integer into a list of digits."
 5       digs = []
 6       while x != 0:
 7           div, mod = divmod(x, 10)
 8           digs.append(mod)
 9  ->       x = mod
 10       return digs
 11
 12   def is_palindrome(x):
 13       "Determine if an integer is a palindrome."
 14       digs = digits(x)
(Pdb)

我们看到这确实是在一个循环中,这证实了我们对可能涉及无限循环的怀疑。

我们可以使用 return 命令尝试运行到当前函数的末尾。如果这不返回,我们将有非常有力的证据证明这是一个无限循环:

(Pdb) r

我们让它运行几秒钟以确认我们从未退出该功能,然后按下Ctrl+C。回到 PDB 提示符后,让我们使用 quit 命令退出 PDB:

(Pdb) quit
%

设置显式中断

既然我们知道问题出在digits()上,那么让我们使用前面提到的pdb.set_trace()函数在其中设置一个显式断点:

def digits(x):
 """Convert an integer into a list of digits.

 Args:
 x: The number whose digits we want.

 Returns: A list of the digits, in order, of ``x``.

 >>> digits(4586378)
 [4, 5, 8, 6, 3, 7, 8]
 """

 import pdb; pdb.set_trace()

 digs = []
 while x != 0:
 div, mod = divmod(x, 10)
 digs.append(mod)
 x = mod
 return digs

记住,set_trace()函数将停止执行并进入调试器。

因此,现在我们可以在不指定 PDB 模块的情况下执行脚本:

% python palindrome.py
> /Users/sixty_north/examples/palindrome.py(8)digits()
-> digs = []
(Pdb)

我们看到,我们几乎立即进入 PDB 提示符,执行在digits()函数开始时停止。

为了验证我们知道我们在哪里,让我们使用 where 来查看我们的调用堆栈:

(Pdb) where
 /Users/sixty_north/examples/palindrome.py(35)<module>()
-> unittest.main()
 /Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/un\
ittest/main.py(95)__init__()
-> self.runTests()
 /Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/uni\
ttest/main.py(229)runTests()
-> self.result = testRunner.run(self.test)
 /Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/uni\
ttest/runner.py(151)run()
-> test(result)
 /Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/uni\
ttest/suite.py(70)__call__()
-> return self.run(*args, **kwds)
 /Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/uni\
ttest/suite.py(108)run()
-> test(result)
 /Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/uni\
ttest/suite.py(70)__call__()
-> return self.run(*args, **kwds)
 /Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/uni\
ttest/suite.py(108)run()
-> test(result)
 /Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/uni\
ttest/case.py(391)__call__()
-> return self.run(*args, **kwds)
 /Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/uni\
ttest/case.py(327)run()
-> testMethod()
/Users/sixty_north/examples/palindrome.py(25)test_negative()
-> self.assertFalse(is_palindrome(1234))
/Users/sixty_north/examples/palindrome.py(17)is_palindrome()
-> digs = digits(x)
> /Users/sixty_north/examples/palindrome.py(8)digits()
-> digs = []

请记住,最新的帧位于此列表的末尾。经过大量的unittest函数,我们看到我们确实在digits()函数中,正如我们所预期的,它被is_palindrome()调用。

逐步执行

我们现在要做的是观察执行情况,看看为什么我们从不退出这个函数的循环。让我们使用“下一步”移动到循环体的第一行:

(Pdb) next
> /Users/sixty_north/examples/palindrome.py(9)digits()
-> while x != 0:
(Pdb) next
> /Users/sixty_north/examples/palindrome.py(10)digits()
-> div, mod = divmod(x, 10)
(Pdb)

现在让我们看看一些变量的值,并尝试决定我们预期会发生什么。我们可以使用 print 命令检查值:

(Pdb) print(digs)
[]
(Pdb) print x
1234

这看起来是正确的。digs列表(将包含结尾的数字序列)是空的,x是我们传入的。我们希望divmod()函数返回1234,所以让我们试试:

(Pdb) next
> /Users/sixty_north/examples/palindrome.py(11)digits()
-> digs.append(mod)
(Pdb) print div,mod
123 4

这看起来是正确的:divmod()函数从我们的数字中删除了最低有效数字,下一行将该数字放入我们的结果列表中:

(Pdb) next
> /Users/sixty_north/examples/palindrome.py(12)digits()
-> x = mod

如果我们查看digs,我们将看到它现在包含 mod:

(Pdb) print digs
[4]

下一行现在将更新x,以便我们可以继续从中剪切数字:

(Pdb) next
> /Users/sixty_north/examples/palindrome.py(9)digits()
-> while x != 0:

我们看到,执行返回到while-循环,正如我们预期的那样。让我们看看x以确保它具有正确的值:

(Pdb) print x
4

等一下!我们希望x保留尚未在结果列表中的数字。相反,它只包含结果列表中的数字。很明显,我们在更新x!时犯了一个错误

如果我们查看代码,很快就会发现我们应该将div而不是mod分配给x。让我们退出 PDB:

(Pdb) quit

请注意,由于 PDB 和unittest的交互方式,您可能需要运行 quit 几次。

修正错误

在您退出 PDB 后,让我们删除set_trace()调用并修改digits()以修复我们发现的问题:

def digits(x):
 """Convert an integer into a list of digits.

 Args:
 x: The number whose digits we want.

 Returns: A list of the digits, in order, of ``x``.

 >>> digits(4586378)
 [4, 5, 8, 6, 3, 7, 8]
 """

 digs = []
 while x != 0:
 div, mod = divmod(x, 10)
 digs.append(mod)
 x = div
 return digs

如果我们现在运行我们的程序,我们会看到我们通过了所有测试,并且它运行得非常快:

$ python palindrome.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.001s

OK

这是一个基本的 PDB 会话,它演示了 PDB 的一些核心功能。然而,PDB 还有许多其他的命令和特性,学习它们的最好方法就是开始使用 PDB 并试用这些命令。这个回文程序可以作为学习 PDB 大多数特性的好例子。

总结

  • Python 的标准调试器称为 PDB。
  • PDB 是标准的命令行调试器。
  • pdb.set_trace()方法可用于停止程序执行 并进入调试器。
  • 当您在调试器中时,REPL 的提示将更改为(Pdb)。
  • 您可以通过键入help来访问 PDB 的内置帮助系统。
  • 您可以使用 python-m pdb后跟脚本名,从一开始就在 PDB 下运行程序。
  • PDB 的where命令显示当前调用堆栈。
  • PDB 的next命令允许执行继续到下一行代码。
  • PDB 的continue命令允许程序无限期地继续执行,或者直到您使用Ctrl+C停止执行为止。
  • PDB 的list命令显示当前位置的源代码。
  • PDB 的return命令继续执行,直到当前函数结束。
  • PDB 的print命令允许您查看调试器中对象的值。
  • 使用quit命令退出 PDB。

一路上我们发现:

  • divmod()函数一次计算除法 运算的商和余数。

  • reversed()功能可以反转顺序。

  • 您可以将-m传递给 Python 命令,使其作为脚本运行模块。

  • Debugging makes it clear that Python is evaluating everything at run time.

    请注意,我们可以使用带括号或不带括号的 print。不要惊慌——我们还没有回归到 Python2。在此上下文中,打印是 PDB命令而不是 Python 3函数