前言

即使您编写了清晰可读的代码,即使您是非常有经验的开发人员,奇怪的bug也不可避免地会出现,您将需要以某种方式调试它们。很多人使用一堆print语句来查看代码中发生了什么。这种方法远不是理想的,有更好的方法可以找出代码的错误所在,本文将探讨其中一些问题和应对方法。


日志是必须的

如果在编写应用程序时没有设置日志记录,那么您最终会后悔的。应用程序中没有任何日志会使故障排除变得非常困难。幸运的是,在Python中,建立基本的日志程序非常简单:

import logginglogging.basicConfig(filename='application.log',level=logging.WARNING,format= '[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s',datefmt='%H:%M:%S')logging.error("Some serious error occurred.")logging.warning('Function you are using is deprecated.')

这就是所有你需要开始写日志的文件,它看起来像这样,你可以找到文件的路径使用

logger . getloggerclass ().root.handlers[0].baseFilename):

[12:52:35] {:1} ERROR - Some serious error occurred.[12:52:35] {:1} WARNING - Function you are using is deprecated.

__repr__ 可读的日志

对代码进行简单的改进,使其更具可调试性,可以在类中添加__repr__方法。如果你不熟悉这个方法-它所做的只是返回一个类实例的字符串表示。使用__repr__方法的最佳实践是输出可用于重新创建实例的文本。例如:

class Circle:def __init__(self, x, y, radius):self.x = xself.y = yself.radius = radiusdef __repr__(self):return f"Rectangle({self.x}, {self.y}, {self.radius})"...c = Circle(100, 80, 30)repr(c)# Circle(100, 80, 30)

除了__repr__,在调用print(实例)时,执行__str__方法也是一个好主意。有了这两种方法,你可以通过打印你的变量得到很多信息。

针对字典的__missing__方法

如果出于某种原因需要实现自定义dictionary类,那么在尝试访问一些实际上不存在的密钥时,您可能会遇到一些由keyerror引起的错误。为了避免在代码中到处查看丢失了哪个键(key),你可以实现特殊的__miss__方法,每次KeyError被提出时调用。

class MyDict(dict):def __missing__(self, key):message = f'{key} not present in the dictionary!'logging.warning(message)return message # Or raise some error instead

上面的实现非常简单,只返回和记录丢失键的消息,但是您还可以记录其他有价值的信息,以便了解代码中出现了什么问题。

调试

崩溃的应用程序

如果您的应用程序在您有机会了解其中发生了什么之前就崩溃了,那么您可能会发现这个技巧非常有用。

使用-i参数运行应用程序(python3 -i app.py)会导致程序一退出就启动交互式shell。此时,您可以检查变量和函数。

如果这还不够好,您可以带一个更强大的工具 - pdb - Python调试器。pdb有很多特性,可以单独写一篇文章来说明。但这里有一个例子和最重要的部分的纲要。让我们先看看崩溃脚本:

# crashing_app.pySOME_VAR = 42class SomeError(Exception):passdef func():raise SomeError("Something went wrong...")func()

现在,如果我们用-i参数运行它,我们就有机会调试它:

# Run crashing application~ $ python3 -i crashing_app.pyTraceback (most recent call last):File "crashing_app.py", line 9, in func()File "crashing_app.py", line 7, in funcraise SomeError("Something went wrong...")__main__.SomeError: Something went wrong...>>> # We are interactive shell>>> import pdb>>> pdb.pm() # start Post-Mortem debugger> .../crashing_app.py(7)func()-> raise SomeError("Something went wrong...")(Pdb) # Now we are in debugger and can poke around and run some commands:(Pdb) p SOME_VAR # Print value of variable42(Pdb) l # List surrounding code we are working with23 class SomeError(Exception):4 pass56 def func():7 -> raise SomeError("Something went wrong...")89 func()[EOF](Pdb) # Continue debugging... set breakpoints, step through the code, etc.

上面的调试会话非常简单地展示了使用pdb可以做什么。程序结束后,我们进入交互式调试会话。首先,导入pdb并启动调试器。此时,我们可以使用所有pdb命令。作为上面的示例,我们使用p命令打印变量,使用l命令列出代码。大部分时间你可能会想要设置断点,可以与b LINE_NO和运行程序,直到断点(c),然后继续与年代,逐页浏览功能的选择可能与w。

堆栈跟踪

假设您的代码是运行在远程服务器上的Flask或Django应用程序,在那里您无法获得交互式调试会话。在这种情况下,你可以使用traceback和sys包来了解你的代码中失败的地方:

import tracebackimport sysdef func():try:raise SomeError("Something went wrong...")except:traceback.print_exc(file=sys.stderr)

在运行时,上面的代码将打印引发的最后一个异常。除了打印异常,您还可以使用traceback包来打印stacktrace (traceback. print_stack())或提取原始堆栈帧,格式化它并进一步检查它(traceback. format_list(traceback.extract_stack()))。

在调试期间重新加载模块

有时,您可能在交互式shell中调试或试验某些函数,并经常对其进行更改。为了使运行/测试和修改的循环更容易,您可以运行importlib.reload(模块),以避免在每次更改后重新启动交互会话:

>>> import func from module>>> func()"This is result..."# Make some changes to "func">>> func()"This is result..." # Outdated result>>> from importlib import reload; reload(module) # Reload "module" after changes made to "func">>> func()"New result..."

这个技巧更多的是关于效率而不是调试。能够跳过一些不必要的步骤,使您的工作流程更快、更高效总是很好的。一般来说,不时地重新加载模块是一个好主意,因为它可以帮助您避免调试已经被修改了很多次的代码。

总结

Debug是一门艺术。