当前位置:   article > 正文

写给程序员的Python教程笔记11——使用PDB进行调试_pdb调试

pdb调试

第 11 章  使用PDB进行调试

        PDB是Python标准库自带的强大调试器,与其他Python调试器相比,PDB的主要优势在于,它是Python的一部分,PDB几乎可以在任何有Python的地方使用,包括嵌入Python语言的大型系统的专用环境,比如ESRI的ArcGIS地理信息系统。

        PDB与其他调试工具不同,不是一个单独的程序,而是一个模块,可以将其导入到任何程序中,使用set_trace()函数在程序执行的任何时刻都可启动调试器。

  1. >>> import pdb
  2. >>> pdb.set_trace()
  3. --Return--
  4. > <stdin>(1)<module>()->None
  5. (Pdb)

        在执行set_trace()之后,提示会从三箭头变成(pdb)——通过这个可以知道已经进入调试器。

11.1 调试命令

        首先通过输入help来查看调试器中可用的命令:

  1. (Pdb) help
  2. Documented commands (type help <topic>):
  3. ========================================
  4. EOF c d h list q rv undisplay
  5. a cl debug help ll quit s unt
  6. alias clear disable ignore longlist r source until
  7. args commands display interact n restart step up
  8. b condition down j next return tbreak w
  9. break cont enable jump p retval u whatis
  10. bt continue exit l pp run unalias where
  11. Miscellaneous help topics:
  12. ==========================
  13. exec pdb

        获得命令的特定帮助:

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

        命令名中的圆括号表示可以通过输入c、cont、或全部单词continue来激活continue命令。

11.2 调试回文程序

        本节来调试一个简单的函数。函数is_palindrome()可接收一个整数,并确定整数是否是回文。回文是正序和反序一样的序列。

        创建palindrome.py:

  1. # palindrome.py
  2. import unittest
  3. def digits(x):
  4. """将整数转换为数字列表。
  5. Args:
  6. x:检查的数字
  7. Returns:数字列表,按照“x”的顺序排列。
  8. >>> digits(4586378)
  9. [4, 5, 8, 6, 3, 7, 8]
  10. """
  11. digs = []
  12. while x != 0:
  13. div, mod = divmod(x, 10)
  14. digs.append(mod)
  15. x = mod
  16. return digs
  17. def is_palindrome(x):
  18. """确定一个整数是否是回文。
  19. Args:
  20. x:需要进行回文检查的数字。
  21. Returns:如果数字“x”是回文数字就返回True,否则返回False。
  22. >>> is_palindrom(1234)
  23. False
  24. >>> is_palindrom(2468642)
  25. True
  26. """
  27. digs = digits(x)
  28. for f, r in zip(digs, reversed(digs)):
  29. if f != r:
  30. return False
  31. return True
  32. class Tests(unittest.TestCase):
  33. """''is_Palindrom()''函数的测试"""
  34. def test_negative(self):
  35. """检查结果为错误返回False。"""
  36. self.assertFalse(is_palindrome(1234))
  37. def test_positive(self):
  38. """检查结果为正确返回True。"""
  39. self.assertTrue(is_palindrome(1234321))
  40. def test_single_digit(self):
  41. """对于一位数字,检查能正常运行。"""
  42. for i in range(10):
  43. self.assertTrue(is_palindrome(i))
  44. if __name__ == '__main__':
  45. unittest.main()

        上列代码有3个主要部分:

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

        运行程序并发现错误,然后来看看如何使用PDB查找错误。

11.2.1 使用PDB找Bug

        运行程序时,程序没有快速地运行完,在一直运行!很明显,程序出现了问题,先用Ctrl+C组合键来中止程序。

        使用命令行调用在PDB的控制下启动程序:

  1. D:\python\写给程序员的Python教程\pyfund>python -m pdb palindrome.py
  2. > d:\python\写给程序员的python教程\pyfund\palindrome.py(4)<module>()
  3. -> import unittest
  4. (Pdb)

        上段代码使用-m参数告诉Python将特定的模块(在这个例子中是PDB)作为脚本,其余的参数会传递给该脚本。所以在这里,我们告诉Python将PDB模块作为一个脚本来执行,并且将文件的名称传递给它。

        会看到PDB提示符。指向import unittest的箭头告诉我们,这是下一条要执行的语句。但是这条语句在哪里?

        使用where命令来找到它:

  1. (Pdb) where
  2. c:\users\letu\appdata\local\programs\python\python311\lib\bdb.py(597)run()
  3. -> exec(cmd, globals, locals)
  4. <string>(1)<module>()
  5. > d:\python\写给程序员的python教程\pyfund\palindrome.py(4)<module>()
  6. -> import unittest

        where 命令报告了当前的调用堆栈,且最近的帧在底部。可以看到,PDB在palindrome.py的第一行暂停执行。这证实了之前讨论过的Python 执行的一个重要方面:所有事情都是在运行时进行求值的。此时,程序在导入语句之前暂停执行。
        可以使用 next 命令运行到下一个语句来执行这个导入:        

  1. (Pdb) next
  2. > d:\python\写给程序员的python教程\pyfund\palindrome.py(6)<module>()
  3. -> def digits(x):
  4. (Pdb)

        可以看到,程序会执行 digits()函数的def 调用。当再次执行 next 时,程序会转到is_palindrome()函数的定义:

  1. (Pdb) next
  2. > d:\python\写给程序员的python教程\pyfund\palindrome.py(30)<module>()
  3. -> def is_palindrome(x):
  4. (Pdb)

        你可能想知道为什么调试器没有进入 digits 函数体。毕竟,它没有像其他部分一样在运行时进行求值。答案是函数的主体只有在提供了参数的情况下才能被求值,所以只有在函数被调用时函数的主体才会运行。在导入时程序会检查函数体的语法是否正确,但是PDB 不允许我们调试语法检查的那一部分。

11.2.2使用抽样找到无限循环

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

        我们不是在单步执行代码,而是顺其自然地执行代码。当认为程序进入循环中时,就使用ctrl+C组合键来重新返回调试器:

  1. (Pdb) next
  2. > d:\python\写给程序员的python教程\pyfund\palindrome.py(26)digits()
  3. -> x = mod
  4. (Pdb) --KeyboardInterrupt--
  5. (Pdb)

        让程序运行几秒后,按 ctrl+C 组合键停止程序,此时程序显示,停在了palindrome.py的digits()函数中。如果想看具体某一行的源代码,可以使用PDB的list命令:

  1. (Pdb) list
  2. 21
  3. 22
  4. 23 while x != 0:
  5. 24 div, mod = divmod(x, 10)
  6. 25 digs.append(mod)
  7. 26 -> x = mod
  8. 27 return digs[::-1]
  9. 28
  10. 29
  11. 30 def is_palindrome(x):
  12. 31 """确定一个整数是否是回文。
  13. (Pdb)

        可以看到,程序确实是一个循环,这证实了我们的怀疑,它可能涉及无限循环。

        可以使用 return 命令来尝试运行到当前函数的结尾。如果不返回任何对象,那么就有非常有力的证据表明该程序是一个无限循环:

(Pdb) r

        让它运行几秒来确认永远不会退出函数,然后按 Ctrl+C 组合键。一旦回到 PDB提示符,可以用quit命令退出PDB:

(Pdb) quit

11.2.3设置显式的断点

        由于我们知道问题在于digits(),因此可以使用前面提到的pdb.set trace()函数在digits()处设置一个显式的断点:

  1. def digits(x):
  2. """将整数转换为数字列表。
  3. Args:
  4. x:检查的数字
  5. Returns:数字列表,按照“x”的倒序排列。
  6. >>> digits(4586378)
  7. ['8', '7', '3', '6', '8', '5', '4']
  8. [4, 5, 8, 6, 3, 7, 8]
  9. """
  10. import pdb; pdb.set_trace()
  11. digs = []
  12. while x != 0:
  13. div, mod = divmod(x, 10)
  14. digs.append(mod)
  15. x = mod
  16. return digs[::-1]

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

        现在可以在不指定PDB模块的情况下执行脚本:

  1. D:\python\写给程序员的Python教程\pyfund>python palindrome.py
  2. > d:\python\写给程序员的python教程\pyfund\palindrome.py(20)digits()
  3. -> digs = []
  4. (Pdb)

        可以看到,程序几乎立即进入了一个PDB提示符,并在digits()函数的开头停止执行。

        为了验证当前所处的位置,使用where来看一下调用堆栈:

  1. (Pdb) where
  2. d:\python\写给程序员的python教程\pyfund\palindrome.py(66)<module>()
  3. -> unittest.main()
  4. c:\users\letu\appdata\local\programs\python\python311\lib\unittest\main.py(102)__init__()
  5. -> self.runTests()
  6. c:\users\letu\appdata\local\programs\python\python311\lib\unittest\main.py(274)runTests()
  7. -> self.result = testRunner.run(self.test)
  8. c:\users\letu\appdata\local\programs\python\python311\lib\unittest\runner.py(217)run()
  9. -> test(result)
  10. c:\users\letu\appdata\local\programs\python\python311\lib\unittest\suite.py(84)__call__()
  11. -> return self.run(*args, **kwds)
  12. c:\users\letu\appdata\local\programs\python\python311\lib\unittest\suite.py(122)run()
  13. -> test(result)
  14. c:\users\letu\appdata\local\programs\python\python311\lib\unittest\suite.py(84)__call__()
  15. -> return self.run(*args, **kwds)
  16. c:\users\letu\appdata\local\programs\python\python311\lib\unittest\suite.py(122)run()
  17. -> test(result)
  18. c:\users\letu\appdata\local\programs\python\python311\lib\unittest\case.py(678)__call__()
  19. -> return self.run(*args, **kwds)
  20. c:\users\letu\appdata\local\programs\python\python311\lib\unittest\case.py(623)run()
  21. -> self._callTestMethod(testMethod)
  22. c:\users\letu\appdata\local\programs\python\python311\lib\unittest\case.py(579)_callTestMethod()
  23. -> if method() is not None:
  24. d:\python\写给程序员的python教程\pyfund\palindrome.py(54)test_negative()
  25. -> self.assertFalse(is_palindrome(1234))
  26. d:\python\写给程序员的python教程\pyfund\palindrome.py(43)is_palindrome()
  27. -> digs = digits(x)
  28. > d:\python\写给程序员的python教程\pyfund\palindrome.py(20)digits()
  29. -> digs = []
  30. (Pdb)

        请注意,最近的帧在这个堆栈的末尾。经过大量的单元测试函数之后,我们看到它确实在digits()函数中,并且被is_palindrome()调用,就像我们所期望的那样。

11.2.4 跳过执行

        本节我们要继续观察执行,看看为什么程序不退出这个函数的循环。使用next移动到循环体的第一行:

  1. (Pdb) next
  2. > d:\python\写给程序员的python教程\pyfund\palindrome.py(23)digits()
  3. -> while x != 0:
  4. (Pdb) next
  5. > d:\python\写给程序员的python教程\pyfund\palindrome.py(24)digits()
  6. -> div, mod = divmod(x, 10)
  7. (Pdb)

        现在来看一些变量的值。并验证是否发生我们期望发生的事情。可以使用print命令来检查值:

  1. (Pdb) print(digs)
  2. []
  3. (Pdb) print(x)
  4. 1234
  5. (Pdb)

        这看起来是正确的。digs列表(保存数字序列)是空的,x是我们传入的。我们期望divmod()函数返回123和4:

  1. (Pdb) next
  2. > d:\python\写给程序员的python教程\pyfund\palindrome.py(25)digits()
  3. -> digs.append(mod)
  4. (Pdb) print (div,mod)
  5. 123 4
  6. (Pdb)

        这看起来是正确的:divmod()函数已经从数字中删除了最低有效位,下一行代码会把这个数字放到结果列表中:

  1. (Pdb) next
  2. > d:\python\写给程序员的python教程\pyfund\palindrome.py(26)digits()
  3. -> x = mod

        查看digs,可以看到它现在包含了mod:

  1. (Pdb) print(digs)
  2. [4]
  3. (Pdb)

        下一行代码将更新x,这样就可以继续剪切digits了:

  1. (Pdb) next
  2. > d:\python\写给程序员的python教程\pyfund\palindrome.py(23)digits()
  3. -> while x != 0:
  4. (Pdb)

        可以看到,执行回到了我们期望的while循环。接下来查看x以确保它的数值正确:

  1. (Pdb) print(x)
  2. 4
  3. (Pdb)

        等一下!我们期望x保存不在结果列表中的数字。但相反,它只保存了结果列表中的数字。显然,我们在更新x时犯了错误!

        如果看看代码,我们很快就会明白应该把div而不是mod分配给x。退出PDB:

(Pdb) quit

        请注意,由于PDB和unittest相互作用,你可能必须执行几次quit才可以退出。

11.2.5 修复BUG

        在退出PDB之后,删除set_trace()调用并修改digits()来解决发现的问题:

  1. def digits(x):
  2. """将整数转换为数字列表。
  3. Args:
  4. x:检查的数字
  5. Returns:数字列表,按照“x”的倒序排列。
  6. >>> digits(4586378)
  7. ['8', '7', '3', '6', '8', '5', '4']
  8. [4, 5, 8, 6, 3, 7, 8]
  9. """
  10. digs = []
  11. while x != 0:
  12. div, mod = divmod(x, 10)
  13. digs.append(mod)
  14. x = div
  15. return digs[::-1]

        如果现在运行这个程序,就会看到它通过了所有的测试,而且运行速度非常快:

  1. D:\python\写给程序员的Python教程\pyfund>python palindrome.py
  2. ...
  3. ----------------------------------------------------------------------
  4. Ran 3 tests in 0.000s
  5. OK

        上列代码是一个基本的PDB会话,它演示了PDB的一些核心功能。PDB有许多其他的命令和功能,但是学习它们的最好方法是开始使用PDB并尝试使用命令。这个回文项目可以作为学习PDB大部分功能的一个很好的例子。

11.3 小结

  • 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()函数可以同时计算除法操作的商和余数。
  • reverse()函数可以反转一个序列。
  • 可以将-m传递给Python命令,让模块作为脚本运行。
  • 调试表明,Python在运行时对所有内容求值。

      

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/482271
推荐阅读
相关标签
  

闽ICP备14008679号