2.3. Debugging code

作者GaëlVaroquaux

本节探讨了如何更好地理解代码库的工具:调试,查找和修复错误。

它不是特定于科学的Python社区,但我们将采用的战略是根据其需要量身定制。

先决条件

2.3.1. 避免错误

2.3.1.1. 编码避免麻烦的最佳做法

  • 我们都编写buggy代码。接受。处理它。
  • 编写你的代码考虑测试和调试。
  • 保持简单,愚蠢(KISS)。
    • 什么是最简单的事情可能工作?
  • 不要重复自己(DRY)。
    • 每一个知识必须在一个系统中具有单一,明确,权威的表示。
    • 常量,算法等...
  • 尝试限制代码的相互依赖性。(松耦合)
  • 给你的变量,函数和模块有意义的名字(不是数学名称)

2.3.1.2. pyflakes:快速静态分析

它们是Python中的几个静态分析工具;举几个例子:

这里我们专注于pyflakes,这是最简单的工具。

  • 快速,简单
  • 检测语法错误,缺少导入,名称上的错误。

另一个好的建议是flake8工具,它是pyflakes和pep8的组合。因此,除了pyflakes捕获的错误类型之外,flake8还会检测PEP8样式指南中是否违反了建议。

强烈建议在编辑器或IDE中集成pyflakes(或flake8),它的确会提高生产率增益

2.3.1.2.1. Running pyflakes on the current edited file

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

  • 在kate菜单:'settings - >配置kate

    • 在插件中启用“外部工具”

    • 在外部工具中,添加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报告

  • In 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)))

2.3.1.2.2. A type-as-go spell-checker like integration

  • In vim

  • 在emacs中使用pyflakes中的flymake模式,在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)

2.3.2. 调试工作流程

如果你有一个不小的bug,这是调试策略踢的时候。没有银弹。然而,战略有助于:

对于调试给定的问题,有利的情况是当问题被隔离在少量的代码行,框架或应用程序代码之外,具有短的修改 - 运行失败周期
  1. 使其可靠地失败。找到使代码每次都失败的测试用例。

  2. 分裂与征服。一旦您有失败的测试用例,请隔离失败的代码。

    • 哪个模块。
    • 哪个功能。
    • 哪一行代码。

    =>隔离一个小的可重现的失败:一个测试用例

  3. 一次更改一个事件,并重新运行失败的测试用例。

  4. 使用调试器来了解出错了什么。

  5. 记笔记和耐心。这可能需要一段时间。

注意

一旦你经历了这个过程:孤立一小段代码再现的错误,并使用这段代码修复错误,添加相应的代码到你的测试套件。

2.3.3. 使用Python调试器

python调试器pdbhttps://docs.python.org/library/pdb.html允许您以交互方式检查代码。

具体来说,它允许您:

  • 查看源代码。
  • 在调用堆栈上向上和向下走。
  • 检查变量的值。
  • 修改变量的值。
  • 设置断点。

打印

是,print语句可以作为调试工具。但是,要检查运行时,使用调试器通常更有效。

2.3.3.1. 调用调试器

启动调试器的方法:

  1. postmortem后,启动调试器模块错误后。
  2. 使用调试器启动模块。
  3. 调用模块中的调试器

2.3.3.1.1. Postmortem

情况:您正在IPython工作,您会收到回溯。

这里我们调试文件index_error.py运行它时,会出现IndexError键入%debug并放入调试器。

In [1]: %run index_error.py
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
/home/varoquau/dev/scipy-lecture-notes/advanced/debugging/index_error.py in <module>()
6
7 if __name__ == '__main__':
----> 8 index_error()
9
/home/varoquau/dev/scipy-lecture-notes/advanced/debugging/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/varoquau/dev/scipy-lecture-notes/advanced/debugging/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的事后调试

在某些情况下,您不能使用IPython,例如调试希望从命令行调用的脚本。在这种情况下,您可以使用python -m pdb script.py t0>:

$ python -m pdb index_error.py
> /home/varoquau/dev/scipy-lecture-notes/advanced/optimizing/index_error.py(1)<module>()
-> """Small snippet to raise an IndexError."""
(Pdb) continue
Traceback (most recent call last):
File "/usr/lib/python2.6/pdb.py", line 1296, in main
pdb._runscript(mainpyfile)
File "/usr/lib/python2.6/pdb.py", line 1215, in _runscript
self.run(statement)
File "/usr/lib/python2.6/bdb.py", line 372, in run
exec cmd in globals, locals
File "<string>", line 1, in <module>
File "index_error.py", line 8, in <module>
index_error()
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/varoquau/dev/scipy-lecture-notes/advanced/optimizing/index_error.py(5)index_error()
-> print lst[len(lst)]
(Pdb)

2.3.3.1.2. Step-by-step execution

情况:您认为模块中存在错误,但不确定在哪里。

例如,我们试图调试wiener_filtering.py的确,代码运行,但过滤不能很好地工作。

  • 使用调试器使用%run -d wiener_filtering.p在IPython中运行脚本:

    In [1]: %run -d wiener_filtering.py
    
    *** Blank or comment
    *** Blank or comment
    *** Blank or comment
    Breakpoint 1 at /home/varoquau/dev/scipy-lecture-notes/advanced/optimizing/wiener_filtering.py:4
    NOTE: Enter 'c' at the ipdb> prompt to start your script.
    > <string>(1)<module>()
  • 使用b 34在第34行设置断点:

    ipdb> n
    
    > /home/varoquau/dev/scipy-lecture-notes/advanced/optimizing/wiener_filtering.py(4)<module>()
    3
    1---> 4 import numpy as np
    5 import scipy as sp
    ipdb> b 34
    Breakpoint 2 at /home/varoquau/dev/scipy-lecture-notes/advanced/optimizing/wiener_filtering.py:34
  • 使用c(ont(inue))继续执行到下一个断点:

    ipdb> c
    
    > /home/varoquau/dev/scipy-lecture-notes/advanced/optimizing/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将跨越执行上下文,即启用内部函数调用:

    ipdb> s
    
    > /home/varoquau/dev/scipy-lecture-notes/advanced/optimizing/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/varoquau/dev/scipy-lecture-notes/advanced/optimizing/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/varoquau/dev/scipy-lecture-notes/advanced/optimizing/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

哦,亲爱的,除了整数,0变化。这里是我们的bug,我们做整数运算。

提高数字错误异常

当我们运行wiener_filtering.py文件时,会出现以下警告:

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

我们可以在异常中转换这些警告,这使我们能够对它们进行事后调试,并更快地找到我们的问题:

In [3]: np.seterr(all='raise')
Out[3]: {'divide': 'print', 'invalid': 'print', 'over': 'print', 'under': 'ignore'}
In [4]: %run wiener_filtering.py
---------------------------------------------------------------------------
FloatingPointError Traceback (most recent call last)
/home/esc/anaconda/lib/python2.7/site-packages/IPython/utils/py3compat.pyc in execfile(fname, *where)
176 else:
177 filename = fname
--> 178 __builtin__.execfile(filename, *where)
/home/esc/physique-cuso-python-2013/scipy-lecture-notes/advanced/debugging/wiener_filtering.py in <module>()
55 pl.matshow(noisy_face[cut], cmap=pl.cm.gray)
56
---> 57 denoised_face = iterated_wiener(noisy_face)
58 pl.matshow(denoised_face[cut], cmap=pl.cm.gray)
59
/home/esc/physique-cuso-python-2013/scipy-lecture-notes/advanced/debugging/wiener_filtering.py in iterated_wiener(noisy_img, size)
38 res = noisy_img - denoised_img
39 noise = (res**2).sum()/res.size
---> 40 noise_level = (1 - noise/l_var )
41 noise_level[noise_level<0] = 0
42 denoised_img += noise_level*res
FloatingPointError: divide by zero encountered in divide

2.3.3.1.3. Other ways of starting a debugger

  • 将异常作为穷人断点

    如果你发现需要注意行号来设置断点,你可以简单地在需要检查和使用IPython的%debug的地方引发一个异常。请注意,在这种情况下,您无法执行步骤或继续执行。

  • 使用nosetests调试测试失败

    您可以运行nosetests - pdb来丢弃对异常的事后调试,以及nosetests - pdb-failure使用调试器检查测试失败。

    此外,您可以通过安装鼻插件ipdbplugin在调试器的鼻子中使用IPython接口。您可以将--ipdb--ipdb-failure选项传递到nosetests。

  • 明确调用调试器

    在调试器中插入以下行:

    import pdb; pdb.set_trace()
    

警告

运行nosetests时,将捕获输出,因此看起来调试器不工作。只需使用-s标志运行nosetests。

图形调试器和替代品

  • 对于单步执行代码和检查变量,您可能会发现使用图形调试器(如winpdb)更为方便。
  • 或者,pudb是一个很好的半图形调试器,在控制台中有一个文本用户界面。
  • 此外,pydbgr项目可能值得一看。

2.3.3.2. 调试器命令和交互

l(list) 列出当前位置的代码
u(p) 走上调用堆栈
d(own) 走下调用堆栈
n(ext) 执行下一行(在新函数中不下行)
s(tep) 执行下一条语句(在新函数中下降)
bt 打印调用堆栈
a 打印局部变量
!command 执行给定的Python命令(通过反对pdb命令)

警告

调试器命令不是Python代码

你不能以你想要的方式命名变量。例如,如果在您不能覆盖当前帧中具有相同名称的变量:在调试器中键入代码时使用与您的本地变量不同的名称

2.3.3.2.1. Getting help when in the debugger

键入hhelp以访问交互式帮助:

ipdb> help
Documented commands (type help <topic>):
========================================
EOF bt cont enable jump pdef r tbreak w
a c continue exit l pdoc restart u whatis
alias cl d h list pinfo return unalias where
args clear debug help n pp run unt
b commands disable ignore next q s until
break condition down j p quit step up
Miscellaneous help topics:
==========================
exec pdb
Undocumented commands:
======================
retval rv

2.3.4. 使用gdb 调试分段故障

如果你有一个分段错误,你不能用pdb调试它,因为它崩溃的Python解释器,在它可以放入调试器。同样,如果你在Python中嵌入了一个C代码的错误,pdb是无用的。为此,我们转到在Linux上可用的gnu调试器gdb

在我们开始使用gdb之前,让我们为它添加一些Python特定的工具。为此,我们在~/.gbdinit中添加了一些宏。宏的最佳选择取决于你的Python版本和你的gdb版本。我在gdbinit中添加了一个简化版本,但随时可以阅读DebuggingWithGdb

要使用gdb调试Python脚本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)

我们得到一个segfault,并且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代码。我们想知道什么是触发这种segfault的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 make_big_array(small_array):
big_array = stride_tricks.as_strided(small_array,
shape=(2e6, 2e6), strides=(32, 32))
return big_array
def print_big_array(small_array):
big_array = make_big_array(small_array)

因此,当打印big_array[-10:]时,会发生segfault。原因很简单,big_array已经被分配,它的结束在程序存储器之外。

注意

有关在gdbinit中定义的特定于Python的命令的列表,请阅读此文件的源代码。


结束练习

以下脚本有详细文档,希望清楚。它试图回答数值计算的实际兴趣的问题,但它不工作...你可以调试它吗?

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)