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 插件

      1. https://www.vim.org/scripts/script.php?script_id=2441 下载 zip 文件

      2. 将文件解压缩到 ~/.vim/ftplugin/python

      3. 确保你的 vimrc 有 filetype plugin indent on

      ../../_images/vim_pyflakes.png
    • 或者:使用 syntastic 插件。这可以配置为使用 flake8,并且还处理许多其他语言的即时检查。

      ../../_images/vim_syntastic.png
  • 在 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. 调试工作流程

如果你确实有一个非平凡的错误,那么这就是调试策略发挥作用的时候。没有银弹。然而,策略是有帮助的

对于调试给定问题,理想的情况是当问题在一个小数量的代码行中被隔离时,在框架或应用程序代码之外,并且具有较短的修改-运行-失败周期

  1. 使它可靠地失败。找到一个测试用例,使代码每次都失败。

  2. 分而治之。一旦你有了失败的测试用例,就隔离失败的代码。

    • 哪个模块。

    • 哪个函数。

    • 哪一行代码。

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

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

  4. 使用调试器来了解哪里出了问题。

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

注意

一旦你完成了这个过程:隔离了一小段代码来重现错误,并使用这段代码修复错误,将相应的代码添加到你的测试套件中。

2.3.3. 使用 Python 调试器

Python 调试器,pdbhttps://docs.pythonlang.cn/3/library/pdb.html,允许你以交互方式检查代码。

具体来说,它允许你

  • 查看源代码。

  • 上下遍历调用堆栈。

  • 检查变量的值。

  • 修改变量的值。

  • 设置断点。

2.3.3.1. 调用调试器

启动调试器的方法

  1. 事后,在模块错误后启动调试器。

  2. 使用调试器启动模块。

  3. 在模块内部调用调试器

事后

情况:你正在 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. 调试器命令和交互

l(ist)

列出当前位置的代码

u(p)

向上遍历调用堆栈

d(own)

向下遍历调用堆栈

n(ext)

执行下一行代码(不会进入新函数)

s(tep)

执行下一个语句(会进入新函数)

bt

打印调用栈

a

打印局部变量

!command

执行给定的 **Python** 命令(与 pdb 命令相反)

警告

调试器命令不是 Python 代码

您不能按您想要的方式命名变量。例如,在您不能用相同名称覆盖当前帧中的变量:**在调试器中键入代码时,使用与您的局部变量不同的名称。**

在调试器中获取帮助

键入 hhelp 访问交互式帮助

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 特定命令的列表,请阅读该文件的内容。