翻译自:http://scipy-lectures.github.com/advanced/debugging/index.html
作者:Gaël Varoquaux
本教程探索工具,以更好地理解你的编程基础:调试,找到并修复错误。
这并不特定适合Python科学计算社区,但是我们采取的策略量身定制。
先决条件
目录
这些是几个Python中的静态分析工具:pylint、pychecker和pyflakes。这里我们重点放在pyflakes上,这是最简单的工具。
高度推荐在你的编辑器中集成pyflakes,它确实产生巨大生产力。
你可以绑定一个键来在当前缓冲区运行pyflakes。
设置 -> 配置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报告上。
在Vim中,在你的vimrc中(将F5绑定到pyflakes上):
autocmd FileType python let ∓ = 'echo "*** running % ***" ; pyflakes %'
autocmd FileType tex,mp,rst,python imap [15~ :make!^M
autocmd FileType tex,mp,rst,python map [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/ftplugin/python
在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,这时调试策略就关键了。这没有银色子弹。然而,策略有帮助:
对调试一个给定的问题,最受欢迎的情况是当问题被隔离成少量几行的代码,在框架和应用之外,伴随的更改-运行-失败循环
注意:一旦你已经完成这个过程:严格隔离一片再生bug的代码并使用这片代码修复它,向你的测试套件中添加相应的代码。
Python调试器,pdb
:http://docs.python.org/library/pdb.html,允许你交互地检查你的代码。
具体来说它允许你:
是的,print确实可以作为一个调试工具。然而在运行时检查的话,使用调试器通常更有效率。
开启调试器的方法:
情形:你使用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 ()
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)()
-> """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 "", line 1, in
File "index_error.py", line 1, in
"""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.
> (1)()
进入wiener_filtering.py并且在34行设置断点:
ipdb> n
> /home/lyy/wiener_filtering.py(4)()
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代码
你不能想当然的命名一个变量。例如,例如,如果在其中,你不能在当前情形下覆盖用同名变量:当在调试器中敲代码时,使用不同的名字而不是你的局部变量。
如果你有一个段错误,你不能通过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
, 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=,
src= , 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=, _nc=), 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= , _nc=), 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=, big_array= ), 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)