2.3. 调试代码¶
作者: Gaël Varoquaux
本节探讨了更好地理解代码库的工具:调试,以查找和修复错误。
它不特定于科学 Python 社区,但我们将采用的策略是针对其需求量身定制的。
2.3.1. 避免错误¶
2.3.1.1. 编码最佳实践以避免陷入困境¶
我们都写过有错误的代码。接受它。处理它。
在编写代码时要考虑测试和调试。
保持简单,愚蠢 (KISS)。
什么是最简单的事情?
不要重复自己 (DRY)。
每个知识片段在系统中都必须有一个单一的、明确的、权威的表示。
常量、算法等。
尝试限制代码的相互依赖性。(松散耦合)
为你的变量、函数和模块赋予有意义的名称(而不是数学名称)
2.3.1.2. pyflakes:快速静态分析¶
Python 中有几种静态分析工具;举几个例子
在这里,我们重点关注 pyflakes,它是最简单的工具。
快速,简单
检测语法错误、缺少导入、名称上的拼写错误。
另一个好的建议是 flake8 工具,它结合了 pyflakes 和 pep8。因此,除了 pyflakes 捕获的错误类型外,flake8 还检测到 PEP8 样式指南中建议的违规行为。
强烈建议在你的编辑器或 IDE 中集成 pyflakes(或 flake8),它**确实能提高生产力**。
在当前编辑的文件上运行 pyflakes¶
你可以将一个键绑定到在当前缓冲区中运行 pyflakes。
在 kate 中菜单:“设置->配置 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 &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 中
使用 pyflakes.vim 插件
从 https://www.vim.org/scripts/script.php?script_id=2441 下载 zip 文件
将文件解压缩到
~/.vim/ftplugin/python
确保你的 vimrc 有
filetype plugin indent on
或者:使用 syntastic 插件。这可以配置为使用
flake8
,并且还处理许多其他语言的即时检查。
在 emacs 中
使用带有 pyflakes 的 flymake 模式,在 https://www.emacswiki.org/emacs/FlyMake 上有文档记录,并且包含在 Emacs 26 和更新版本中。要激活它,使用
M-x
(元键然后是 x),并在提示符下输入 flymake-mode。要在打开 Python 文件时自动启用它,请将以下行添加到你的 .emacs 文件中(add-hook 'python-mode-hook '(lambda () (flymake-mode)))
2.3.2. 调试工作流程¶
如果你确实有一个非平凡的错误,那么这就是调试策略发挥作用的时候。没有银弹。然而,策略是有帮助的
对于调试给定问题,理想的情况是当问题在一个小数量的代码行中被隔离时,在框架或应用程序代码之外,并且具有较短的修改-运行-失败周期
使它可靠地失败。找到一个测试用例,使代码每次都失败。
分而治之。一旦你有了失败的测试用例,就隔离失败的代码。
哪个模块。
哪个函数。
哪一行代码。
=> 隔离一个小的可重现的故障:一个测试用例
一次更改一件事,然后重新运行失败的测试用例。
使用调试器来了解哪里出了问题。
做笔记,要有耐心。这可能需要一段时间。
注意
一旦你完成了这个过程:隔离了一小段代码来重现错误,并使用这段代码修复错误,将相应的代码添加到你的测试套件中。
2.3.3. 使用 Python 调试器¶
Python 调试器,pdb
:https://docs.pythonlang.cn/3/library/pdb.html,允许你以交互方式检查代码。
具体来说,它允许你
查看源代码。
上下遍历调用堆栈。
检查变量的值。
修改变量的值。
设置断点。
2.3.3.1. 调用调试器¶
启动调试器的方法
事后,在模块错误后启动调试器。
使用调试器启动模块。
在模块内部调用调试器
事后¶
情况:你正在 IPython 中工作,并且你得到一个回溯。
这里我们调试文件 index_error.py
。运行它时,会引发一个 IndexError
。键入 %debug
并进入调试器。
In [1]: %run index_error.py
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
File ~/src/scientific-python-lectures/advanced/debugging/index_error.py:10
6 print(lst[len(lst)])
9 if __name__ == "__main__":
---> 10 index_error()
File ~/src/scientific-python-lectures/advanced/debugging/index_error.py:6, in index_error()
4 def index_error():
5 lst = list("foobar")
----> 6 print(lst[len(lst)])
IndexError: list index out of range
In [2]: %debug
> /home/jarrod/src/scientific-python-lectures/advanced/debugging/index_error.py(6)index_error()
4 def index_error():
5 lst = list("foobar")
----> 6 print(lst[len(lst)])
7
8
ipdb> list
1 """Small snippet to raise an IndexError."""
2
3
4 def index_error():
5 lst = list("foobar")
----> 6 print(lst[len(lst)])
7
8
9 if __name__ == "__main__":
10 index_error()
ipdb> len(lst)
6
ipdb> print(lst[len(lst) - 1])
r
ipdb> quit
逐步执行¶
情况:你认为模块中存在错误,但不知道在哪里。
例如,我们正在尝试调试 wiener_filtering.py
。实际上,代码运行了,但过滤效果不好。
使用调试器在 IPython 中运行脚本,使用
%run -d wiener_filtering.py
In [1]: %run -d wiener_filtering.py *** Blank or comment *** Blank or comment *** Blank or comment NOTE: Enter 'c' at the ipdb> prompt to continue execution. > /home/jarrod/src/scientific-python-lectures/advanced/debugging/wiener_filtering.py(1)<module>() ----> 1 """Wiener filtering a noisy raccoon face: this module is buggy""" 2 3 import numpy as np 4 import scipy as sp 5 import matplotlib.pyplot as plt
使用
b 29
在第 29 行设置断点ipdb> n > /home/jarrod/src/scientific-python-lectures/advanced/debugging/wiener_filtering.py(3)<module>() 1 """Wiener filtering a noisy raccoon face: this module is buggy""" 2 ----> 3 import numpy as np 4 import scipy as sp 5 import matplotlib.pyplot as plt ipdb> b 29 Breakpoint 1 at /home/jarrod/src/scientific-python-lectures/advanced/debugging/wiener_filtering.py:29
使用
c(ont(inue))
继续执行到下一个断点ipdb> c > /home/jarrod/src/scientific-python-lectures/advanced/debugging/wiener_filtering.py(29)iterated_wiener() 27 Do not use this: this is crappy code to demo bugs! 28 """ 1--> 29 noisy_img = noisy_img 30 denoised_img = local_mean(noisy_img, size=size) 31 l_var = local_var(noisy_img, size=size)
使用
n(ext)
和s(tep)
进入代码:next
跳到当前执行上下文中的下一条语句,而step
会跨执行上下文,即能够在函数调用内部进行探索ipdb> s > /home/jarrod/src/scientific-python-lectures/advanced/debugging/wiener_filtering.py(30)iterated_wiener() 28 """ 1 29 noisy_img = noisy_img ---> 30 denoised_img = local_mean(noisy_img, size=size) 31 l_var = local_var(noisy_img, size=size) 32 for i in range(3): ipdb> n > /home/jarrod/src/scientific-python-lectures/advanced/debugging/wiener_filtering.py(31)iterated_wiener() 1 29 noisy_img = noisy_img 30 denoised_img = local_mean(noisy_img, size=size) ---> 31 l_var = local_var(noisy_img, size=size) 32 for i in range(3): 33 res = noisy_img - denoised_img
执行几行并探索局部变量
ipdb> n > /home/jarrod/src/scientific-python-lectures/advanced/debugging/wiener_filtering.py(32)iterated_wiener() 30 denoised_img = local_mean(noisy_img, size=size) 31 l_var = local_var(noisy_img, size=size) ---> 32 for i in range(3): 33 res = noisy_img - denoised_img 34 noise = (res**2).sum() / res.size ipdb> print(l_var) [[2571 2782 3474 ... 3008 2922 3141] [2105 708 475 ... 469 354 2884] [1697 420 645 ... 273 236 2517] ... [2437 345 432 ... 413 387 4188] [2598 179 247 ... 367 441 3909] [2808 2525 3117 ... 4413 4454 4385]] ipdb> print(l_var.min()) 0
哎呀,只有整数,而且变化为 0。这就是我们的错误,我们正在进行整数运算。
其他启动调试器的方法¶
引发异常作为穷人断点
如果你觉得记下要设置断点的行号很麻烦,你可以简单地在要检查的地方引发一个异常,并使用 IPython 的
%debug
。请注意,在这种情况下,你无法执行步骤或继续执行。使用 nosetests 调试测试失败
你可以运行
nosetests --pdb
以在异常上进行事后调试,并运行nosetests --pdb-failure
以使用调试器检查测试失败。此外,你可以通过安装 nose 插件 ipdbplugin 来使用 nose 中的 IPython 调试器接口。然后,你可以将
--ipdb
和--ipdb-failure
选项传递给 nosetests。显式调用调试器
在要进入调试器的地方插入以下行
import pdb; pdb.set_trace()
警告
在运行 nosetests
时,输出会被捕获,因此调试器似乎不起作用。只需使用 -s
标志运行 nosetests。
2.3.3.2. 调试器命令和交互¶
|
列出当前位置的代码 |
|
向上遍历调用堆栈 |
|
向下遍历调用堆栈 |
|
执行下一行代码(不会进入新函数) |
|
执行下一个语句(会进入新函数) |
|
打印调用栈 |
|
打印局部变量 |
|
执行给定的 **Python** 命令(与 pdb 命令相反) |
警告
调试器命令不是 Python 代码
您不能按您想要的方式命名变量。例如,在您不能用相同名称覆盖当前帧中的变量:**在调试器中键入代码时,使用与您的局部变量不同的名称。**
在调试器中获取帮助¶
键入 h
或 help
访问交互式帮助
ipdb> help
Documented commands (type help <topic>):
========================================
EOF commands enable ll pp s until
a condition exceptions longlist psource skip_hidden up
alias cont exit n q skip_predicates w
args context h next quit source whatis
b continue help p r step where
break d ignore pdef restart tbreak
bt debug j pdoc return u
c disable jump pfile retval unalias
cl display l pinfo run undisplay
clear down list pinfo2 rv unt
Miscellaneous help topics:
==========================
exec pdb
Undocumented commands:
======================
interact
2.3.4. 使用 gdb 调试段错误¶
如果您遇到段错误,则无法使用 pdb 调试它,因为它会在进入调试器之前崩溃 Python 解释器。类似地,如果您在 Python 中嵌入的 C 代码中存在 bug,pdb 毫无用处。为此,我们转向 gnu 调试器,gdb,在 Linux 上可用。
在我们开始使用 gdb 之前,让我们为它添加一些 Python 特定的工具。为此,我们向我们的 ~/.gdbinit
添加一些宏。宏的最佳选择取决于您的 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)
我们得到一个段错误,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 make_big_array(small_array):
big_array = stride_tricks.as_strided(
small_array, shape=(int(2e6), int(2e6)), strides=(32, 32)
)
return big_array
因此,段错误发生在打印 big_array[-10:]
时。原因很简单,因为 big_array
已被分配,其末尾位于程序内存之外。
注意
有关在 gdbinit 中定义的 Python 特定命令的列表,请阅读该文件的内容。