盒子
盒子
文章目录
  1. 调试代码
    1. 避免错误
      1. 避免麻烦最佳编码法
      2. pyflakes:快速静态分析
        1. 在当前编辑文件运行pyflakes
        2. 一个在线拼写检查集成
    2. 调试工作流
    3. 使用Python调试器
      1. 调用调试器
        1. 尸检
        2. 单步执行
        3. 其它开始调试的方法
      2. 调试器命令和交互
    4. 使用gdb调试段错误
    5. 总结练习
    6. Footnotes

调试代码(Python)

调试代码

翻译自:http://scipy-lectures.github.com/advanced/debugging/index.html

作者:Gaël Varoquaux

本教程探索工具,以更好地理解你的编程基础:调试,找到并修复错误。

这并不特定适合Python科学计算社区,但是我们采取的策略量身定制。

先决条件

目录

  • toc
    {: toc}

避免错误

避免麻烦最佳编码法

  • 我们都写有bug的代码。接受它,解决它。
  • 写程序时,在心里测试和调试。
  • Keep it Simple, Stupid(KISS).
    • 可能有效的最简单方法
  • Don‘t Repeat Yourself (DRY).
    • 每个知识都必须有个单独、明确、在一个系统中的正式表示
    • 常量、算法,等等。
  • 尝试限制你的代码间的相互关系(松耦合)
  • 赋予你的变量、函数和模块有意义的名字(不是数学名称)

pyflakes:快速静态分析

这些是几个Python中的静态分析工具:pylintpycheckerpyflakes。这里我们重点放在pyflakes上,这是最简单的工具。

  • 快速,简单
  • 探测语法错误,漏掉的导入,名称的拼写错误。

高度推荐在你的编辑器中集成pyflakes,它确实产生巨大生产力

在当前编辑文件运行pyflakes

你可以绑定一个键来在当前缓冲区运行pyflakes。

  • kate中 菜单:设置 -> 配置kate

    • 在插件中勾选’外部工具’^1
    • 在外部工具中,添加pyflakes:

      kdialog --title "pyflakes %filename" --msgbox "$(pyflakes %filename)"
      
  • 在TextMate中
    菜单:TextMate -> 首选项 -> 高级 -> Shell变量,添加一个shell变量:

    TM_PYCHECKER=/Library/Frameworks/Python.framework/Versions/Current/bin/pyflakes
    

    然后Ctrl-Shift-V被绑定到pyflakes报告上。

  • Vim中,在你的vimrc中(将F5绑定到pyflakes上):

    autocmd FileType python let &mp = 'echo "*** running % ***" ; pyflakes %'
    autocmd FileType tex,mp,rst,python imap <Esc>[15~ <C-O>:make!^M
    autocmd FileType tex,mp,rst,python map  <Esc>[15~ :make!^M
    autocmd FileType tex,mp,rst,python set autowrite]]    
    
  • 在emacs中,在你的.emacs中(绑定F5到pyflakes):

    (defun pyflakes-thisfile () (interactive)
       (compile (format "pyflakes %s" (buffer-file-name)))
    )
    
    (define-minor-mode pyflakes-mode
        "Toggle pyflakes mode.
        With no argument, this command toggles the mode.
        Non-null prefix argument turns on the mode.
        Null prefix argument turns off the mode."
        ;; The initial value.
        nil
        ;; The indicator for the mode line.
        " Pyflakes"
        ;; The minor mode bindings.
        '( ([f5] . pyflakes-thisfile) )
    )
    
    (add-hook 'python-mode-hook (lambda () (pyflakes-mode t)))
    

一个在线拼写检查集成

  • 在vim中使用vim.pyflakes插件:
    1. http://www.vim.org/scripts/script.php?script_id=2441下载zip文件^2
    2. 将文件解压到.vim/ftplugin/python
    3. 确定你的vimrc中有’filetype plugin indent on’
  • 在emacs中在flymake模式中使用pyflakes,文档在http://www.plope.com/Members/chrism/flymake-mode:添加以下内容到你的.emacs文件:

    (when (load "flymake" t)
            (defun flymake-pyflakes-init ()
            (let* ((temp-file (flymake-init-create-temp-buffer-copy
                                'flymake-create-temp-inplace))
                (local-file (file-relative-name
                            temp-file
                            (file-name-directory buffer-file-name))))
                (list "pyflakes" (list local-file))))
    
            (add-to-list 'flymake-allowed-file-name-masks
                    '("\\.py\\'" flymake-pyflakes-init)))
    
    (add-hook 'find-file-hook 'flymake-find-file-hook)
    

调试工作流

你确实有个并不微不足道的bug,这时调试策略就关键了。这没有银色子弹。然而,策略有帮助:

对调试一个给定的问题,最受欢迎的情况是当问题被隔离成少量几行的代码,在框架和应用之外,伴随的更改-运行-失败循环

  1. 让出错可靠。找到一个测试实例让代码每次都出错(fail)
  2. 分解并且克服。一旦你有了一个失败测试实例,隔绝出错代码 。
    • 哪个模块
    • 哪个函数
    • 哪行代码
      = > 隔离小块可再生错误:测试样例。
  3. 一次更改一样东西,并且重新运行错误测试样例
  4. 使用调试器来理解哪里出错了
  5. 记笔记,保持耐心。这将花很久。

注意:一旦你已经完成这个过程:严格隔离一片再生bug的代码并使用这片代码修复它,向你的测试套件中添加相应的代码。

使用Python调试器

Python调试器,pdbhttp://docs.python.org/library/pdb.html,允许你交互地检查你的代码。

具体来说它允许你:

  • 查看源代码
  • 向上或向下查看调用堆栈
  • 检查变量值
  • 更改变量值
  • 设置断点

print

是的,print确实可以作为一个调试工具。然而在运行时检查的话,使用调试器通常更有效率。

调用调试器

开启调试器的方法:

  1. 尸检(post-mortem),在模块错误后开启调试器
  2. 用调试器运行模块
  3. 在模块中调用调试器

尸检

情形:你使用ipython并且你获得一个回溯(traceback)。

现在让我们调试文件index_error.py。当运行它时,一个IndexError被引发。键入%debug将进入调试器:

In [1]: %run index_error.py
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
/usr/lib/python2.7/site-packages/IPython/utils/py3compat.py in execfile(fname, *where)
    176             else:
    177                 filename = fname
--> 178             __builtin__.execfile(filename, *where)

/home/lyy/index_error.py in <module>()
      6 
      7 if __name__ == '__main__':
----> 8     index_error()
      9 

/home/lyy/index_error.py in index_error()
      3 def index_error():
      4     lst = list('foobar')
----> 5     print lst[len(lst)]
      6 
      7 if __name__ == '__main__':

IndexError: list index out of range

In [2]: %debug
> /home/lyy/index_error.py(5)index_error()
      4     lst = list('foobar')
----> 5     print lst[len(lst)]
      6 

ipdb> list
      1 """Small snippet to raise an IndexError."""
      2 
      3 def index_error():
      4     lst = list('foobar')
----> 5     print lst[len(lst)]
      6 
      7 if __name__ == '__main__':
      8     index_error()
      9 

ipdb> len(lst)
6
ipdb> print lst[len(lst)-1]
r
ipdb> quit

In [3]: 

在IPython之外尸检^3调试

在一些情形下你无法使用IPython,例如调试一个想要从命令行调用的脚本。这中情况下,你可以使用python -m pdb script.py来调用脚本:

lyy@arch ~ % python2 -m pdb index_error.py
> /home/lyy/index_error.py(1)<module>()
-> """Small snippet to raise an IndexError."""
(Pdb) continue
Traceback (most recent call last):
  File "/usr/lib/python2.7/pdb.py", line 1314, in main
    pdb._runscript(mainpyfile)
  File "/usr/lib/python2.7/pdb.py", line 1233, in _runscript
    self.run(statement)
  File "/usr/lib/python2.7/bdb.py", line 387, in run
    exec cmd in globals, locals
  File "<string>", line 1, in <module>
  File "index_error.py", line 1, in <module>
    """Small snippet to raise an IndexError."""
  File "index_error.py", line 5, in index_error
    print lst[len(lst)]
IndexError: list index out of range
Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
> /home/lyy/index_error.py(5)index_error()
-> print lst[len(lst)]
(Pdb) print lst[len(lst)]
*** IndexError: list index out of range

单步执行

情形:你相信在模块中存在一个bug但不确定在哪里。

例如我们尝试调试wiener_filtering.py。确实代码运行了,但是滤波效果并不好。

  • 用调试器运行脚本:

    In [1]: %run -d wiener_filtering.py
    *** Blank or comment
    *** Blank or comment
    *** Blank or comment
    Breakpoint 1 at /home/lyy/wiener_filtering.py:4
    NOTE: Enter 'c' at the ipdb>  prompt to start your script.
    > <string>(1)<module>()
    
  • 进入wiener_filtering.py并且在34行设置断点:

    ipdb> n
    > /home/lyy/wiener_filtering.py(4)<module>()
          3 
    1---> 4 import numpy as np
          5 import scipy as sp
    
    ipdb> b 34
    Breakpoint 2 at /home/lyy/wiener_filtering.py:34
    
  • c(out(inue))继续执行到下一个断点:

    ipdb> c
    > /home/lyy/wiener_filtering.py(34)iterated_wiener()
         33     """
    2--> 34     noisy_img = noisy_img
         35     denoised_img = local_mean(noisy_img, size=size)
    
  • n(ext)s(tep)回到代码:next跳到当前执行文本的下一个语句,而step将穿越执行文本,也就是,可以深入探索函数调用:^4

    ipdb> s
    > /home/lyy/wiener_filtering.py(35)iterated_wiener()
    2    34     noisy_img = noisy_img
    ---> 35     denoised_img = local_mean(noisy_img, size=size)
         36     l_var = local_var(noisy_img, size=size)
    
    ipdb> n
    > /home/lyy/wiener_filtering.py(36)iterated_wiener()
         35     denoised_img = local_mean(noisy_img, size=size)
    ---> 36     l_var = local_var(noisy_img, size=size)
         37     for i in range(3):
    
  • 执行几行然后探索局部变量:

    ipdb> n
    > /home/lyy/wiener_filtering.py(37)iterated_wiener()
         36     l_var = local_var(noisy_img, size=size)
    ---> 37     for i in range(3):
         38         res = noisy_img - denoised_img
    
    ipdb> print l_var
    [[5868 5379 5316 ..., 5071 4799 5149]
     [5013  363  437 ...,  346  262 4355]
     [5379  410  344 ...,  392  604 3377]
     ..., 
     [ 435  362  308 ...,  275  198 1632]
     [ 548  392  290 ...,  248  263 1653]
     [ 466  789  736 ..., 1835 1725 1940]]
    ipdb> print l_var.min()
    0
    

Oh,亲爱的,除了整数什么也没有,还有0变动。这就是我们的bug所在,我们在做整数算术。

引发数值错误例外

当我们运行wiener_filtering.py文件时,以下警告被引发(raise):

In [1]: %run wiener_filtering.py
wiener_filtering.py:40: RuntimeWarning: divide by zero encountered in divide
  noise_level = (1 - noise/l_var )

我们可以将这些警告转化为例外(exception),这让我们能对它们尸检调试,并且迅速找到我们的问题:

In [2]: np.seterr(all='raise')
Out[2]: {'divide': 'warn', 'invalid': 'warn', 'over': 'warn', 'under': 'ignore'}

其它开始调试的方法

  • 引发一个例外作为”可怜的”断点

    如果你发现几下行号设置断点很无聊,你可以简单地在你想要检查的地方引发一个例外,使用ipython的%debug。注意在这种情况下你不能step和continue执行。

  • 使用nosetests调试测试失败

    你可以运行nosetests --pdb对例外(exception)尸检调试,并且nosetests --pdb-failure使用调试器来检查测试失败。

    另外,你可以通过安装nose插件ipdbplugin,在nose中使用IPython调试接口。你可以传递--ipdb--ipdb-failure选项给nosetests。

  • 显式调用调试器

    在你想要调试的地方放入以下一行:

    import pdb; pdb.set_trace()
    
警告:当运行nosetests时,输出被捕捉,并且因此似乎调试器不工作了。只要加上-s标志运行nosetests

图形化调试器

为了单步执行和检查变量,你也许觉得用图形化的调试器如winpdb更加方便。

另外,pudb是一个很棒的半图形化调试器,它在终端里有一个文字用户界面。

调试器命令和交互

命令 作用
l(list) 列出当前运行的代码
u(p) 上一个调用堆栈
d(own) 下一个调用堆栈
n(ext) 执行下一行(不到新的函数)
s(tep) 执行下一个语句(到新的函数)
bt 打印调用堆栈
a 打印局部变量
!command 执行特定的Python命令(相对于pdb命令)

警告:Debugger 命令不是Python代码

你不能想当然的命名一个变量。例如,例如,如果在其中,你不能在当前情形下覆盖用同名变量:当在调试器中敲代码时,使用不同的名字而不是你的局部变量

使用gdb调试段错误

如果你有一个段错误,你不能通过pdb调试它,因为它会让Python解释器在进入调试器之前崩溃。同样,如果你在Python中嵌入的C代码有bug,pdb也没用。对这种情况我们使用在linux上可用的gnu调试器gdb

在我们用gdb之前,让我们向它添加一些Python特有的工具。对此我们向我们的gdbinit添加一些宏。宏优化的选择依赖于你的Python版本和gdb版本。我已经在gdbinit里添加了一个简化的版本,但是请自由地阅读DebuggingWithGdb

为用gdb调试脚本segfault.py,我们可以在gdb中如下运行:

$ gdb python
...
(gdb) run segfault.py
Starting program: /usr/bin/python segfault.py
[Thread debugging using libthread_db enabled]

Program received signal SIGSEGV, Segmentation fault.
_strided_byte_copy (dst=0x8537478 "\360\343G", outstrides=4, src=
    0x86c0690 <Address 0x86c0690 out of bounds>, instrides=32, N=3,
    elsize=4)
        at numpy/core/src/multiarray/ctors.c:365
365            _FAST_MOVE(Int32);
(gdb)

我们得到一个段错误,并且gdb事后在C级别的堆栈中(不是python调用堆栈)捕捉到了它。我们可以使用使用gdb的命令调试C调用堆栈:

(gdb) up
#1  0x004af4f5 in _copy_from_same_shape (dest=<value optimized out>,
    src=<value optimized out>, myfunc=0x496780 <_strided_byte_copy>,
    swap=0)
at numpy/core/src/multiarray/ctors.c:748
748         myfunc(dit->dataptr, dest->strides[maxaxis],

如你所见,现在我们在numpy的C代码中。我们想要知道什么Python代码触发了这个段错误,因此我们向上查看堆栈直到我们发现Python执行循环:

(gdb) up
#8  0x080ddd23 in call_function (f=
    Frame 0x85371ec, for file /home/varoquau/usr/lib/python2.6/site-packages/numpy/core/arrayprint.py, line 156, in _leading_trailing (a=<numpy.ndarray at remote 0x85371b0>, _nc=<module at remote 0xb7f93a64>), throwflag=0)
    at ../Python/ceval.c:3750
3750    ../Python/ceval.c: No such file or directory.
        in ../Python/ceval.c

(gdb) up
#9  PyEval_EvalFrameEx (f=
    Frame 0x85371ec, for file /home/varoquau/usr/lib/python2.6/site-packages/numpy/core/arrayprint.py, line 156, in _leading_trailing (a=<numpy.ndarray at remote 0x85371b0>, _nc=<module at remote 0xb7f93a64>), throwflag=0)
    at ../Python/ceval.c:2412
2412    in ../Python/ceval.c
(gdb)

一旦我们在python执行循环中时,我们可以使用特殊的python帮助函数。例如我们找到相应的Python代码:

(gdb) pyframe
/home/varoquau/usr/lib/python2.6/site-packages/numpy/core/arrayprint.py (158): _leading_trailing
(gdb)

这是numpy的代码,我们需要向上直到找到我们自己写的代码:

(gdb) up
...
(gdb) up
#34 0x080dc97a in PyEval_EvalFrameEx (f=
    Frame 0x82f064c, for file segfault.py, line 11, in print_big_array (small_array=<numpy.ndarray at remote 0x853ecf0>, big_array=<numpy.ndarray at remote 0x853ed20>), throwflag=0) at ../Python/ceval.c:1630
1630    ../Python/ceval.c: No such file or directory.
        in ../Python/ceval.c
(gdb) pyframe
segfault.py (12): print_big_array

相应的代码是:

def print_big_array(small_array):
    big_array = make_big_array(small_array)
    print big_array[-10:]
    return big_array

所以当打印big_array[-10:]时发生段错误。原因很简单,big-array的末尾被分配到程序内存之外了。^5

注意:在gdbinit中定义的专用于python的命令,请阅读这个文件。^6

总结练习

以下脚本清晰易读,它致力于解决数值计算感兴趣的实际问题。但是它不能工作。你能调试它吗?

Python源码to_debug.py

"""
A script to compare different root-finding algorithms.

This version of the script is buggy and does not execute. It is your task
to find an fix these bugs.

The output of the script sould look like:

    Benching 1D root-finder optimizers from scipy.optimize:
                brenth:   604678 total function calls
                brentq:   594454 total function calls
                ridder:   778394 total function calls
                bisect:  2148380 total function calls
"""
from itertools import product

import numpy as np
from scipy import optimize

FUNCTIONS = (np.tan,  # Dilating map
             np.tanh, # Contracting map
             lambda x: x**3 + 1e-4*x, # Almost null gradient at the root
             lambda x: x+np.sin(2*x), # Non monotonous function
             lambda x: 1.1*x+np.sin(4*x), # Fonction with several local maxima
            )

OPTIMIZERS = (optimize.brenth, optimize.brentq, optimize.ridder,
              optimize.bisect)


def apply_optimizer(optimizer, func, a, b):
    """ Return the number of function calls given an root-finding optimizer, 
        a function and upper and lower bounds.
    """
    return optimizer(func, a, b, full_output=True)[1].function_calls,


def bench_optimizer(optimizer, param_grid):
    """ Find roots for all the functions, and upper and lower bounds
        given and return the total number of function calls.
    """
    return sum(apply_optimizer(optimizer, func, a, b)
               for func, a, b in param_grid)


def compare_optimizers(optimizers):
    """ Compare all the optimizers given on a grid of a few different
        functions all admitting a signle root in zero and a upper and
        lower bounds.
    """
    random_a = -1.3 + np.random.random(size=100)
    random_b =   .3 + np.random.random(size=100)
    param_grid = product(FUNCTIONS, random_a, random_b)
    print "Benching 1D root-finder optimizers from scipy.optimize:"
    for optimizer in OPTIMIZERS:
        print '% 20s: % 8i total function calls' % (
                    optimizer.__name__, 
                    bench_optimizer(optimizer, param_grid)
                )


if __name__ == '__main__':
    compare_optimizers(OPTIMIZERS)

Footnotes