Why I need to use PDB to debug Python script

现在各种强大的Python IDE都已经很好地实现了debug的功能,那么我们为啥还需要这样一种命令行的工具来进行debug呢。

这是个好问题,因为我也一直是用Pycharm来调试Python脚本的,感觉也非常方便好用。直到最近,在部署一个服务器应用的时候出现了问题,这个问题在本地无法重现。当然,我首先想到的是使用Remote Debugging来进行调试(参见Remote debugging with Pycharm),毕竟比较熟悉了。然而Remote Debugging有以下问题是比较麻烦的:

  • 本地环境和远程环境不一致(包括操作系统、Python编译器版本、Python package版本等)
  • 一旦远程或本地的代码有所改动,会影响debug的断点位置
  • 需要的配置有些小复杂(比如要配置本地和远程的代码路径的mapping)

因此,在遇到上述问题时(尤其是环境不一致的问题),登陆到远程机器使用PDB来debug是一个不错的选择。

简单的说,PDB有如下优势:

  • 不需要安装,源自Python标准库
  • 纯命令行操作(这点在没有可视界面的操作系统上非常重要)
  • 非常容易上手(虽然相比IDE debug起来不那么直观)

How to debug with PDB

Basic usage

  • l (list) 显示断点所在的上下文代码。

  • p (print) 显示某个变量在断点处的内容。 既然直接输入变量回车就能把这个变量打印出来,为啥还需要用p来打印变量呢?在我debug python的logging lib的时候发现了原因:

    def callHandlers(self, record):
        c = self
        found = 0
        while c:
            for hdlr in c.handlers:
                found = found + 1
                if record.levelno >= hdlr.level:
                    hdlr.handle(record)
                    if not c.propagate:
                        c = None    #break out
                        else:
                            c = c.parent
                            if (found == 0) and raiseExceptions and not self.manager.emittedNoHandlerWarning:
                                sys.stderr.write("No handlers could be found for logger"
                                                 " \"%s\"\n" % self.name)
                                self.manager.emittedNoHandlerWarning = 1
    

    在debug到第二行时,我想知道变量c的值时,直接输入c是不行的(会直接到达下一个断点),此时就需要输入p c来显示变量c的值了。即,对PDB的关键字和保留字必须用p来显示变量内容。

  • n (next) 单步执行代码,相当于Pycharm的Step Over

  • s (step) 单步进入断点所在函数,相当于Pycharm的Step Into

  • c (continue) 直接到达下个断点处,相当于Pycharm的Resume Program

  • b (break) 动态添加断点,用法为b后面加上行号。

  • u (up) 进入上一层的stack trace的frame,这在断点停留在抛出的异常时非常有用,使用这个命令我们可以查看抛出异常时的一些变量状态。

  • d (down) 和上面的命令相反,进入下一层的stack trace的frame。

  • q (quit) 结束调试。

Get into debugger when an exception occurs

这个功能在Pycharm中debug时经常会用到,即当程序抛出异常时可以直接进入debug界面。

下面的例子是stackoverflow的一个回答,当执行这段脚本时,会直接进入PDB的debug页面并且break在print a[0]这一行。

import pdb, traceback, sys

def bombs():
    a = []
    print a[0]

if __name__ == '__main__':
    try:
        bombs()
    except:
        type, value, tb = sys.exc_info()
        traceback.print_exc()
        pdb.post_mortem(tb)

Enhance PDB

Enhance PDB with IPython

既然用到了命令行操作,第一个想到的就是IPython。如果能在PDB的时候用上IPython的丰富的色彩和提示功能启不是很爽?

使用ipdb

原本以为只要把原来的python myscript.py改成ipython myscript.py就行了,结果并没有变化。搜了一下,已经有人做了PDB和IPython的整合,项目就叫ipdb,可以直接使用pip install ipdb来安装,然后把原来代码中的所有pdb都改为ipdb即可。比如说上面debug exception的例子,变成如下就可以了:

import ipdb, traceback, sys

def bombs():
    a = []
    print a[0]

if __name__ == '__main__':
    try:
        bombs()
    except:
        type, value, tb = sys.exc_info()
        traceback.print_exc()
        ipdb.post_mortem(tb)

当然ipdb还对pdb做了一些其他的enhancement,可以到它的github页面查看。

直接使用IPython

如果并不想安装ipdb,直接用IPython来debug也是可以的(毕竟ipdb主要也只是整合了IPython和pdb而已)。参考IPython的官方文档,可以在进入IPython的交互界面后,使用%run -d myscript命令来debug一个脚本。

举个例子,这是我们要执行的脚本myscript.py的内容:

def bombs():
    a = []
    print a[0]


if __name__ == '__main__':
    bombs()

进入IPython的交互界面后,执行%run -d myscript.py,即可直接进入带有IPython功能的PDB的界面,且断点在代码第一行:

In [1]: %run -d myscript.py
Breakpoint 1 at /Users/CYu/Code/Python/python-demo/myscript.py:1
NOTE: Enter 'c' at the ipdb>  prompt to continue execution.
> /Users/CYu/Code/Python/python-demo/myscript.py(1)<module>()
1---> 1 def bombs():
      2     a = []
      3     print a[0]
      4 
      5 

ipdb> 

注意到上面的debug界面pdb也被替换成了ipdb,但其实和之前说的ipdb没啥关系。

在IPython中想要debug exception也很方便,完全不需要在myscript.py中添加任何代码,直接使用%run执行脚本后,再使用%debug就能“恢复”进入到异常所在处进行PDB:

In [1]: %run myscript.py
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
/Users/CYu/Code/Python/python-demo/myscript.py in <module>()
      5 
      6 if __name__ == '__main__':
----> 7     bombs()

/Users/CYu/Code/Python/python-demo/myscript.py in bombs()
      1 def bombs():
      2     a = []
----> 3     print a[0]
      4 
      5 

IndexError: list index out of range

In [2]: %debug
> /Users/CYu/Code/Python/python-demo/myscript.py(3)bombs()
      1 def bombs():
      2     a = []
----> 3     print a[0]
      4 
      5 

ipdb> 

如果觉得每次有异常了输入%debug也有点麻烦,可以使用%pdb命令来打开自动debug异常的模式(相当于每次有异常了自动帮你打%debug):

In [1]: %pdb
Automatic pdb calling has been turned ON

In [2]: %run demo_pdb.py
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
/Users/CYu/Code/Python/python-demo/demo_pdb.py in <module>()
      5 
      6 if __name__ == '__main__':
----> 7     bombs()

/Users/CYu/Code/Python/python-demo/demo_pdb.py in bombs()
      1 def bombs():
      2     a = []
----> 3     print a[0]
      4 
      5 

IndexError: list index out of range
> /Users/CYu/Code/Python/python-demo/demo_pdb.py(3)bombs()
      1 def bombs():
      2     a = []
----> 3     print a[0]
      4 
      5 

ipdb> 

Enhance PDB to debug multiprocess

PDB不够强大的一点就在于它没法debug multiprocess的代码,强行debug也是报错,原因是其他进程的stdin/out/err等文件对主进程而言是关闭的,pdb无法调用。而Pycharm之所以可以debug multiprocess的代码是因为它使用了Remote Debugging的技术,通过远程通信来进行debug(所以debug一个multiprocess的程序和debug一个远程机器上的代码技术实现上是一样的)。

如果还是想使用PDB来debug multiprocess的话,可以参考最简单方法远程调试Python多进程子程序PDB远程调试Python多进程子程序这两篇文章。简单的说,就是通过管道或socket来传递debug的信息,从而使得PDB跨进程也可以使用(感觉近似于自己实现了类似Remote Debugging的功能)。

如果觉得这样做比较麻烦的话,就要移步使用别的带有Remote Debugging功能的库了。

小结

如果想要快速地在远程服务器上debug Python代码,请使用PDB。

如果想舒服地使用PDB,请使用IPython。

如果想debug multiprocess的代码,需要更复杂的设置(但debug起来并不复杂)或是使用其他已经实现了Remote Debugging的工具。