Python调试技巧
#python #调试 #开发人员 #softwareengineering

可以在此处找到本文的'代码':https://github.com/peter-mcconnell/petermcconnell.com/blob/main/assets/dummy/pydebug/main.py

调试Python-上下文

这是我面对新的Python代码库时所采取的流程。我经常发现自己必须调试我从未见过的代码库,这迫使我变得非常自在,因为代码迷失了,并开发了一些帮助我找到自己的方式的模式。这就是我今天与您分享的内容。

我应该注意,我生活在终端中 - 不断连接到服务器,容器,同事机器,我自己的Homelab等,以使我选择的编辑还居住在终端(Neovim)中。因此,本指南是基于终端的,因此不包括基于IDE的调试流(根据我所看到的,这是牢固的)。

有什么要求?

选择的调试器(对我来说)是ipdb。原因是本文的结尾。

安装ipdb

pip3 install --user ipdb

我们还需要从下面的refining scope部分收集信息。

精炼范围

经常(我自己的用户酶)我的python调试故事通常以:“这个应用程序破裂。它在做x”,这对我的看法很少,看什么是什么问题和何处。我的第一个目标是使问题陈述的规模尽可能小 /紧。为此,在我查看任何代码之前,我都尝试执行以下操作:

  • 验证它似乎是代码的问题并将其分类
    • 完美问题
    • 逻辑问题
    • 片状
    • 依赖性问题
    • etc
  • 确定我需要调试的该应用程序的哪个版本以及可以获得的位置
  • 确定代码库的哪一部分(文件位置,方法,行)
  • 标识所需的输入(方法参数,环境变量,第三方来源等)
  • 了解已经尝试解决问题的方法
  • 确定利益相关者,紧迫性等...

这有一些目的:

  • 确保我可以重现错误
  • 减少我需要看的事物的范围
  • 帮助我了解业务逻辑 /预期结果< / li>

在这一点上,我应该有信心知道问题需要调试。

示例应用程序

开始创建以下文件。这是我可以创建的最简单的示例,以便保持信号/噪声比以支持实际调试步骤:

#!/usr/bin/env python
# main.py
def doubleit(val):
    return val * 3

if __name__ == "__main__":
    print("doubleit 2: %d", doubleit(2))
    print("doubleit 4: %d", doubleit(4))
    print("doubleit 8: %d", doubleit(8))

我们将使用此简单示例进行调试。

使用IPDB

从之前收集的信息中可以想象,输出是上面的程序正在吐出错误的值。我们希望doubleit行显示它们的值加倍,但是它们似乎是三倍的(是的,很明显为什么,但是想象一下这是一个很大的程序,您不知道为什么输出就是这样)。

使用该信息要交手,我们可以查找doubleit方法并添加设置一些断点,以便我们可以探索该程序以了解状态时:

#!/usr/bin/env python
# main.py
def doubleit(val):
    import ipdb       # < added this line
    ipdb.set_trace()  # < added this line
    return val * 3

if __name__ == "__main__":
    print("doubleit 2: %d", doubleit(2))
    print("doubleit 4: %d", doubleit(4))
    print("doubleit 8: %d", doubleit(8))

我们可以在整个代码中继续添加ipdb.set_trace()点。一般来说,当我第一次运行它时,我倾向于在代码库中掉下一两个点,我知道将会在路径上,希望我会手动踏上执行它如何流动。当我们添加了所有需要的断点时,我们可以指示程序使用python main.py

$ python main.py
> /home/pete/go/src/github.com/peter-mcconnell/petermcconnell.com/assets/dummy/pydebug/main.py(6)doubleit()
      5     ipdb.set_trace()
----> 6     return val * 3
      7

ipdb>

现在,我们已经使用附带的调试器运行了程序,并且已在我们设置的断点处停止执行。我们可以运行args,以查看哪些参数传递给方法:

> /home/pete/go/src/github.com/peter-mcconnell/petermcconnell.com/assets/dummy/pydebug/main.py(6)doubleit()
      5     ipdb.set_trace()
----> 6     return val * 3
      7

ipdb> args
val = 2

因此,在程序中的这一点上,当它被val值为2时,我们使用doubleit方法。我们可以使用p打印此和其他变量:

ipdb> p val
2

或单独使用变量名称:

ipdb> val
2

我们甚至可以从这一点调用方法:

ipdb> doubleit(6)
18

要浏览执行,我们可以按n进行执行点:

ipdb> doubleit(6)
18
ipdb> n
--Return--
6
> /home/pete/go/src/github.com/peter-mcconnell/petermcconnell.com/assets/dummy/pydebug/main.py(6)doubleit()
      5     ipdb.set_trace()
----> 6     return val * 3
      7

并使用bt查看回溯:

ipdb> bt
  /home/pete/go/src/github.com/peter-mcconnell/petermcconnell.com/assets/dummy/pydebug/main.py(9)<module>()
      8 if __name__ == "__main__":
----> 9     print("doubleit 2: %d", doubleit(2))
     10     print("doubleit 4: %d", doubleit(4))

6
> /home/pete/go/src/github.com/peter-mcconnell/petermcconnell.com/assets/dummy/pydebug/main.py(6)doubleit()
      5     ipdb.set_trace()
----> 6     return val * 3
      7

要查看当前执行点附近的代码,只需按l

ipdb> l
      1 #!/usr/bin/env python
      2 # main.py
      3 def doubleit(val):
      4     import ipdb
      5     ipdb.set_trace()
----> 6     return val * 3
      7
      8 if __name__ == "__main__":
      9     print("doubleit 2: %d", doubleit(2))
     10     print("doubleit 4: %d", doubleit(4))
     11     print("doubleit 8: %d", doubleit(8))

当然,哪个显示我们很难找到逻辑错误,* 3而不是* 2

注意:您还可以在stdlib函数中设置断点(路径会根据您的设置而变化):

ipdb> b /home/pete/.local/lib/python3.10/site-packages/requests/api.py:14
Breakpoint 1 at /home/pete/.local/lib/python3.10/site-packages/requests/api.py:14

调试流程

使用上面的命令,我可以在修复程序上开始我的循环过程:

repro->探索 - >理解 - >调整 - >重复

通常,这意味着我只需要了解应用程序的一小部分,并且可以忽略与直接问题无关的代码。

在更详细的级别上,此过程看起来像:

  • (repro)编写一个测试,以简单的术语触发该错误,我可以表达
  • (探索)设置断点
  • (探索)用-s标志运行pytest,以便我可以与ipdb互动
  • (探索)使用args检查我在
  • 中的方法的参数
  • (探索)围绕变量值的打印
  • (探索)确保程序的状态对于我当前的断点是有意义的。如果没有,我需要一个更早的断点。如果是这样,请继续使用n
  • (探索)重复以下步骤,直到我达到该程序处于看似错误的状态的地步
    • (理解)在此阶段,我将花一些时间正确阅读周围的代码并尝试可变值,以查看我是否可以让程序以预期的方式行事
    • (理解)根据错误的类别,我将寻找算法复杂性问题,堆栈溢出问题,参数edgecases,日志记录质量,随机性因素等。这是编辑器设置的闪耀时。请参阅Neovim部分
    • (调整)我将对代码进行较小的调整

一旦我很高兴我的小调整具有理想的效果

Neo

本节描述了我的neovim配置,用于高级调试。简而言之,我的调试 /代码探索流量归结为:< / p>

  • telescope https://github.com/nvim-telescope/telescope.nvim
    • 允许我使用文件扫描目录
    • 允许我设置扫描任何常见目录的键遇到
  • coc https://github.com/neoclide/coc.nvim
    • 代码在我需要的所有语言中完成
    • 功能描述
  • gd-默认的vim键键入,用于定义。将我跳入我想了解的功能
  • ctrl + o / ctrl + i-默认的vim键键键,转到下一个 /下一个跳跃点。我的扫描代码确实很有用 - 我可以继续使用gd进行定义,然后ctrl + o我返回 / ctrl + i我试图构建一个理解< / li>,而我又回到了下降。

您可以在这里看到我的完整Neovim配置:https://github.com/peter-mcconnell/.dotfiles/blob/master/config/nvim/init.vim

概括

上面的示例非常微不足道 - ipdb和它的质量是在复杂的用途上,您甚至可能不知道输入和输出之间的方法,例如调试stdlib。就在本周,我使用ipdb来确定为什么长期以来遗忘的代码库为给定数据集丢下了一个晦涩难懂的错误。通过使用ipdb,我重现了场景,在我知道它会发生错误的点之前,我可以检查程序状态并更好地理解导致错误的条件,从而导致快速补丁。

为什么不PDB?

铃铛和哨子;我喜欢IPDB具有更好的颜色支持和选项卡的完成。您绝对可以使用pdb获得相同的结果。