[关闭]
@ProX 2020-06-03T15:20:12.000000Z 字数 7016 阅读 312

Python终极调试指南

InfoQ


摘要

本文介绍了Python调试的一些高级技巧。如果你还在像新手一样无脑print调试,那么赶紧向大牛学习一下如何优雅地调试Python代码。

正文

作为经验丰富的开发人员,即便你编写了清晰易读的代码,并对代码进行了全方位的测试,但在某些时候程序还是会不可避免地出现一些奇怪的Bug,这时候你需要以某种方式进行Debug。不少程序员诉诸于使用一堆print语句来查看代码运行情况。这种方法有点低级,十分傻瓜,实际上有很多更好的方法来帮你定位代码中的问题,我们将在本文中介绍这些方法。

使用Logging模块

如果你编写的应用程序没有使用日志功能,那你最终会后悔没有及时使用它。如果应用程序中没有打印任何运行日志,那么很难对程序错误进行故障定位及排除。幸运的是在Python中,配置基本的日志模块非常简单:

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

这就是开始将日志写入文件的所有操作,使用时,你可以通过logging.getLoggerClass().root.handlers[0].baseFilename找到文件的路径):

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

这种设置看起来似乎已经足够好了(通常是这样),但是配置合理,格式清晰,可读性强的日志可以让你Debug起来更加轻松。优化日志配置的一种方法是使用.ini或.yaml配置文件。下面给你推荐一种配置示例:

  1. version: 1
  2. disable_existing_loggers: true
  3. formatters:
  4. standard:
  5. format: "[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s"
  6. datefmt: '%H:%M:%S'
  7. handlers:
  8. console: # handler which will log into stdout
  9. class: logging.StreamHandler
  10. level: DEBUG
  11. formatter: standard # Use formatter defined above
  12. stream: ext://sys.stdout
  13. file: # handler which will log into file
  14. class: logging.handlers.RotatingFileHandler
  15. level: WARNING
  16. formatter: standard # Use formatter defined above
  17. filename: /tmp/warnings.log
  18. maxBytes: 10485760 # 10MB
  19. backupCount: 10
  20. encoding: utf8
  21. root: # Loggers are organized in hierarchy - this is the root logger config
  22. level: ERROR
  23. handlers: [console, file] # Attaches both handler defined above
  24. loggers: # Defines descendants of root logger
  25. mymodule: # Logger for "mymodule"
  26. level: INFO
  27. handlers: [file] # Will only use "file" handler defined above
  28. propagate: no # Will not propagate logs to "root" logger

在python代码中使用这种通用的配置将很难编辑和维护。将配置内容保存在YAML文件中,通过加载配置文件的形式,我们就可以避免上述问题,后续也可以很轻松的修改日志配置。

如果你想知道所有这些配置字段的含义,可以从此文档中进行查看,它们中的大多数只是关键字参数,如上面展示的示例所示。

所以,我们已经在配置文件中定义好了日志组件的相关配置,接下来我们需要以某种方式加载该配置。如果使用的是YAML配置文件,最简单地加载配置的方法如下所示:

  1. import yaml
  2. from logging import config
  3. with open("config.yaml", 'rt') as f:
  4. config_data = yaml.safe_load(f.read())
  5. config.dictConfig(config_data)

Python logger实际上并不直接支持YAML文件,但它支持字典配置,可以使用yaml.safe_load从YAML文件轻松创建字典配置。如果你倾向于使用.ini文件,那么我只想指出,对于新应用程序,很多文档都建议使用字典配置。有关更多示例,可以查看使用手册

使用日志装饰器

继续前面的讲到的日志模块技巧,你可能会遇到这么一种情况,就是想debug函数调用执行的情况。你可以使用日志装饰器而不用修改函数主体代码来实现:

  1. from functools import wraps, partial
  2. import logging
  3. def attach_wrapper(obj, func=None): # Helper function that attaches function as attribute of an object
  4. if func is None:
  5. return partial(attach_wrapper, obj)
  6. setattr(obj, func.__name__, func)
  7. return func
  8. def log(level, message): # Actual decorator
  9. def decorate(func):
  10. logger = logging.getLogger(func.__module__) # Setup logger
  11. formatter = logging.Formatter(
  12. '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
  13. handler = logging.StreamHandler()
  14. handler.setFormatter(formatter)
  15. logger.addHandler(handler)
  16. log_message = f"{func.__name__} - {message}"
  17. @wraps(func)
  18. def wrapper(*args, **kwargs): # Logs the message and before executing the decorated function
  19. logger.log(level, log_message)
  20. return func(*args, **kwargs)
  21. @attach_wrapper(wrapper) # Attaches "set_level" to "wrapper" as attribute
  22. def set_level(new_level): # Function that allows us to set log level
  23. nonlocal level
  24. level = new_level
  25. @attach_wrapper(wrapper) # Attaches "set_message" to "wrapper" as attribute
  26. def set_message(new_message): # Function that allows us to set message
  27. nonlocal log_message
  28. log_message = f"{func.__name__} - {new_message}"
  29. return wrapper
  30. return decorate
  31. # Example Usage
  32. @log(logging.WARN, "example-param")
  33. def somefunc(args):
  34. return args
  35. somefunc("some args")
  36. somefunc.set_level(logging.CRITICAL) # Change log level by accessing internal decorator function
  37. somefunc.set_message("new-message") # Change log message by accessing internal decorator function
  38. somefunc("some args")

说实话,这可能需要花一些时间来装饰被调用函数(实际上,你需要做的仅仅是复制粘贴一下就好了)。它的巧妙之处在于通过log函数设置参数,并将参数用于内部wrapper函数。然后,通过添加附加到装饰器的访问器函数使这些参数可调。至于functools.wraps装饰器,如果我们在这里不使用它,被装饰的函数的名称(func .__ name__)将被装饰器的名称所覆盖。在这里我们需要functools.wraps装饰器,因为我们debug时要使用真实的函数名称。它的原理是拷贝原始函数名称,函数文档描述以及参数列表到装饰器函数上。

无论如何,这是上面代码的输出。看起来很整洁吧?

  1. 2020-05-01 14:42:10,289 - __main__ - WARNING - somefunc - example-param
  2. 2020-05-01 14:42:10,289 - __main__ - CRITICAL - somefunc - new-message

重写对象的__repr__

对代码进行小小的改进以使其更易于调试,可以在类中添加__repr__方法。它的功能就是返回类实例的字符串表示形式。__repr__方法的最佳实践是输出可用于重新创建实例的文本。例如:

  1. class Circle:
  2. def __init__(self, x, y, radius):
  3. self.x = x
  4. self.y = y
  5. self.radius = radius
  6. def __repr__(self):
  7. return f"Rectangle({self.x}, {self.y}, {self.radius})"
  8. ...
  9. c = Circle(100, 80, 30)
  10. repr(c)
  11. # Circle(100, 80, 30)

如果不希望或不能像上面那样表示对象,另一个好的方法是使用<...>表示,例如<_io.TextIOWrapper name='somefile.txt' mode='w' encoding='UTF-8'>

除了__repr__以外,重写__str__方法也是一个好方法,该方法在使用print(instance)时被默认调用。使用这两种方法,你只需打印变量即可获得很多信息。

重写字典类的__missing__方法

如果出于某种原因你需要实现自定义字典类,那么当你尝试访问实际上不存在的键时,可能会因KeyErrors引起一些错误。为了避免在debug代码没有头绪,可以实现__missing__这一特殊方法,该方法在每次引发KeyError时都会被调用。

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

上面的实现非常简单,仅返回并记录缺少键的消息,但是你也可以记录其他有价值的信息,以便给你提供更多的有关代码出问题时的上下文。

调试崩溃的应用程序

如果应用程序崩溃后你才有机会查看其中发生的情况,那么你可能会发现下面这个技巧非常有用。

你需要使用-i参数(python3 -i app.py)运行应用程序,该参数会使该程序在程序退出后立即启动并进入交互式shell。 此时,你可以检查当前环境下的变量和函数。

如果这还不够好,那么你可以使用更厉害的pdb,即Python Debuggerpdb具有很多功能,这些功能足以保证撰写一篇长文来介绍。下面给出一个示例,我只摘抄了最重要的部分。首先让我们看一下崩溃的脚本:

  1. # crashing_app.py
  2. SOME_VAR = 42
  3. class SomeError(Exception):
  4. pass
  5. def func():
  6. raise SomeError("Something went wrong...")
  7. func()

现在,如果我们使用-i参数运行它,我们将有机会对其进行调试:

  1. # Run crashing application
  2. ~ $ python3 -i crashing_app.py
  3. Traceback (most recent call last):
  4. File "crashing_app.py", line 9, in <module>
  5. func()
  6. File "crashing_app.py", line 7, in func
  7. raise SomeError("Something went wrong...")
  8. __main__.SomeError: Something went wrong...
  9. >>> # We are interactive shell
  10. >>> import pdb
  11. >>> pdb.pm() # start Post-Mortem debugger
  12. > .../crashing_app.py(7)func()
  13. -> raise SomeError("Something went wrong...")
  14. (Pdb) # Now we are in debugger and can poke around and run some commands:
  15. (Pdb) p SOME_VAR # Print value of variable
  16. 42
  17. (Pdb) l # List surrounding code we are working with
  18. 2
  19. 3 class SomeError(Exception):
  20. 4 pass
  21. 5
  22. 6 def func():
  23. 7 -> raise SomeError("Something went wrong...")
  24. 8
  25. 9 func()
  26. [EOF]
  27. (Pdb) # Continue debugging... set breakpoints, step through the code, etc.

上面的调试会话非常清晰地显示了可以使用pdb进行的操作。程序终止后,我们进入交互式调试会话。 首先,我们导入pdb并启动调试器。此时我们可以使用所有的pdb命令。 在上面的示例中,我们使用p命令打印变量,并使用l命令列出代码。 大多数时候,你可能希望设置断点,可以使用b LINE_NO来设置断点,然后运行程序直到断点(c)被暂停,然后继续使用s逐步执行该函数,还可以选择使用w打印堆栈信息。有关命令的完整列表,可以查阅pdb使用文档

检查堆栈信息

假设你的代码是在远程服务器上运行的Flask或Django应用程序,你无法获得交互式调试会话。 在这种情况下,您可以借助tracebacksys软件包来更深入地了解代码中发生的异常:

  1. import traceback
  2. import sys
  3. def func():
  4. try:
  5. raise SomeError("Something went wrong...")
  6. except:
  7. traceback.print_exc(file=sys.stderr)

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

调试过程中重新加载模块

有时你可能正在调试或在交互式Shell中对一些方法函数进行测试,并对其进行一些修改。 为了简化代码的运行/测试和修改,可以运行importlib.reload(module)以避免每次更改后都必须重新启动交互式会话:

  1. >>> import func from module
  2. >>> func()
  3. "This is result..."
  4. # Make some changes to "func"
  5. >>> func()
  6. "This is result..." # Outdated result
  7. >>> from importlib import reload; reload(module) # Reload "module" after changes made to "func"
  8. >>> func()
  9. "New result..."

这一技巧更多地是关于效率而不是调试。 它可以帮助你跳过一些不必要的步骤,让你的工作更快,更高效。 通常,实时重新加载模块这一功能非常棒,因为它可以帮助你避免调试同时修改过很多次的代码,节省宝贵时间。

作者简介

Martin Heinz

开发运维工程师,现就职与IBM。

原文链接

Ultimate Guide to Python Debugging

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注