Python 调试技术

PythonBeginner
立即练习

介绍

在计算机编程中,调试(debugging)是查找和修复阻止程序正确运行的缺陷或“bug”的基本过程。虽然 bug 是软件开发中不可避免的一部分,但 Python 提供了一套强大的工具来帮助你高效地诊断和修复它们。

在这个 Lab 中,你将学习几种 Python 中基本的调试技术。我们将涵盖如何:

  • 手动引发(raise)和处理(handle)异常,以在发生错误时控制程序流程。
  • 将详细的错误回溯(traceback)捕获为字符串,用于日志记录。
  • 使用断言(assertions)作为健全性检查(sanity checks),尽早捕获逻辑错误。
  • 实现一个灵活的日志记录系统,用于跟踪程序的执行并诊断问题,而不会中断其流程。

引发异常

在这一步,你将学习如何使用 raise 语句手动触发异常。异常是用来指示错误发生的机制。一个 raise 语句由 raise 关键字、对 Exception() 函数的调用,以及传递给 Exception() 的一个有用的错误信息字符串组成。

在函数的前置条件不满足时,在函数内部触发异常,然后在调用该函数的代码中使用 try 和 except 块来处理这些异常,是一种常见的做法。

注意: 如果你不熟悉使用 WebIDE(类似 VS Code),请先在 这个 Lab 学习。

让我们通过一个例子来尝试一下。

  1. 从左侧的文件浏览器中打开文件 raise_exception.py。
  2. 将以下代码添加到文件中。这段代码定义了一个函数 box_print,它对无效参数触发异常,然后使用有效和无效的输入调用此函数,并捕获异常。
def box_print(symbol, width, height):
    if len(symbol) != 1:
      raise Exception('Symbol must be a single character string.')
    if width <= 2:
      raise Exception('Width must be greater than 2.')
    if height <= 2:
      raise Exception('Height must be greater than 2.')
    print(symbol * width)
    for i in range(height - 2):
        print(symbol + (' ' * (width - 2)) + symbol)
    print(symbol * width)

## Handle exceptions when calling the function
for sym, w, h in (('*', 4, 4), ('O', 20, 5), ('x', 1, 3), ('ZZ', 3, 3)):
    try:
        box_print(sym, w, h)
    except Exception as err:  ## Catch exception and print error message
        print('An exception happened: ' + str(err))
  1. 现在,从终端运行脚本,查看异常是如何被触发和处理的。
python3 raise_exception.py

你将看到以下输出。前两次对 box_print 的调用成功,而后两次失败,触发了 except 块来打印自定义的错误信息。

****
*  *
*  *
****
OOOOOOOOOOOOOOOOOOOO
O                  O
O                  O
O                  O
OOOOOOOOOOOOOOOOOOOO
An exception happened: Width must be greater than 2.
An exception happened: Symbol must be a single character string.

将回溯信息获取为字符串

当发生未处理的异常时,Python 会打印一个回溯(traceback),它是导致错误的函数调用链的详细报告。有时,你希望优雅地处理异常,但仍记录完整的堆栈跟踪以供后续调试。traceback 模块允许你执行此操作。

在这一步中,你将学习使用 traceback.format_exc() 将回溯捕获为字符串并将其保存到文件中。

  1. 从文件浏览器中打开文件 get_traceback.py。
  2. 添加以下代码。此脚本导入 traceback 模块,然后使用 try...except 块来捕获异常。在 except 块内部,它将完整的堆栈跟踪写入名为 errorInfo.txt 的文件中。
import traceback

try:
    raise Exception('This is the error message.')
except:
    with open('errorInfo.txt', 'w') as error_file:
        error_file.write(traceback.format_exc())  ## Write traceback to file
    print('The traceback info was written to errorInfo.txt.')
  1. 在终端中执行脚本。
python3 get_traceback.py

脚本将打印一条确认消息:

The traceback info was written to errorInfo.txt.
  1. 现在,检查新创建的 errorInfo.txt 文件的内容。你可以从文件浏览器中打开它,或者在终端中使用 cat 命令。
cat errorInfo.txt

该文件将包含完整的堆栈跟踪,类似于以下内容:

Traceback (most recent call last):
  File "/home/labex/project/get_traceback.py", line 4, in <module>
    raise Exception('This is the error message.')
Exception: This is the error message.

这项技术对于在生产应用中记录错误而又不使程序崩溃非常有用。

使用断言

断言是用于确保你的代码没有明显错误的一种“健全性检查”(sanity checks)。assert 语句会检查一个条件是否为真。如果条件为假,它会引发一个 AssertionError,理想情况下这应该会使你的程序崩溃。这种“快速失败”(fail-fast)的方法可以帮助你更快地确定 Bug 的根本原因。

一个 assert 语句由 assert 关键字、一个要检查的条件以及一个可选的错误消息组成。

在这一步中,你将使用一个断言来验证程序的某个状态。

  1. 从文件浏览器中打开文件 use_assertions.py。
  2. 添加以下代码。它检查 pod_bay_door_status 的状态。第一个断言会静默通过,但第二个断言会失败并引发一个 AssertionError。
## assert statement: check condition, raise AssertionError if False
pod_bay_door_status = 'open'
assert pod_bay_door_status == 'open', 'The pod bay doors need to be "open".'  ## Passes

pod_bay_door_status = 'I\'m sorry, Dave. I\'m afraid I can\'t do that.'
assert pod_bay_door_status == 'open', 'The pod bay doors need to be "open".'  ## Raises AssertionError
  1. 在终端中运行脚本。
python3 use_assertions.py

由于第二个断言失败了,程序将崩溃,你将看到带有自定义消息的 AssertionError。

Traceback (most recent call last):
  File "/home/labex/project/use_assertions.py", line 6, in <module>
    assert pod_bay_door_status == 'open', 'The pod bay doors need to be "open".'
AssertionError: The pod bay doors need to be "open".

与异常不同,你不应该使用 try...except 来处理断言。AssertionError 信号表明程序中存在需要修复的 Bug。值得注意的是,通过使用 -O(优化)标志运行 Python,可以全局禁用断言,因此它们不应用于必须始终强制执行的输入验证。

实现日志记录

虽然 print() 语句可用于调试,但更强大和灵活的方法是使用 Python 内置的 logging 模块。日志记录允许你在程序运行时跟踪事件,并按严重性对消息进行分类。

在这一步中,你将向程序添加日志记录功能,以帮助诊断 Bug。

  1. 首先,从文件浏览器中打开文件 factorial_log.py。
  2. 添加以下代码。此程序尝试计算一个数的阶乘,但其中包含一个 Bug。我们添加了 logging.debug() 调用来跟踪每一步中变量的值。logging.basicConfig() 函数配置日志系统,以显示级别为 DEBUG 及以上的消息。
import logging
logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname)s - %(message)s')
logging.debug('Start of program')

def factorial(n):
    logging.debug('Start of factorial(%s)' % (n))
    total = 1
    for i in range(1, n + 1):
        total *= i
        logging.debug('i is ' + str(i) + ', total is ' + str(total))
    logging.debug('End of factorial(%s)' % (n))
    return total

print(factorial(5))
logging.debug('End of program')
  1. 在终端中运行脚本。
python3 factorial_log.py

你将看到详细的日志输出与程序的常规输出交错显示。

 %(asctime)s - DEBUG - Start of program
 %(asctime)s - DEBUG - Start of factorial(5)
 %(asctime)s - DEBUG - i is 1, total is 1
 %(asctime)s - DEBUG - i is 2, total is 2
 %(asctime)s - DEBUG - i is 3, total is 6
 %(asctime)s - DEBUG - i is 4, total is 24
 %(asctime)s - DEBUG - i is 5, total is 120
 %(asctime)s - DEBUG - End of factorial(5)
120
 %(asctime)s - DEBUG - End of program

(注意:原始 markdown 中的示例代码存在一个 Bug。此处提供的代码是经过修正的,以提供更好的学习体验。原始 Bug 是 range(n + 1) 从 0 开始,导致 total 始终为 0。修复方法是使用 range(1, n + 1)。)

通过检查日志消息,你可以清楚地看到循环中每次迭代时 i 和 total 的值,这是理解程序流程和查找 Bug 的强大方法。

配置日志记录行为

logging 模块具有高度的可配置性。你可以控制要显示的日志消息的严重性级别,完全禁用日志记录,或者将日志消息重定向到文件而不是屏幕。

日志级别(Logging Levels)

有五个标准的日志级别,每个级别都有一个对应的函数:

级别 日志函数 描述
DEBUG logging.debug() 用于详细信息,仅在诊断问题时有用。
INFO logging.info() 用于记录一般事件或确认事情按预期工作。
WARNING logging.warning() 用于指示可能存在但不阻止程序运行的问题。
ERROR logging.error() 用于记录导致特定操作失败的错误。
CRITICAL logging.critical() 用于即将导致程序终止的致命错误。

当你在 basicConfig 中设置一个日志级别时,Python 将显示该级别及所有更高优先级的消息。例如,设置 level=logging.INFO 将显示 INFO、WARNING、ERROR 和 CRITICAL 消息,但不会显示 DEBUG 消息。

禁用日志记录

在调试完成后,你可能希望关闭所有日志消息,而无需从代码中删除 logging 调用。你可以使用 logging.disable() 来实现这一点。它接受一个级别作为参数,并禁用该级别及以下的所有日志消息。例如,logging.disable(logging.CRITICAL) 将抑制所有日志消息。

日志记录到文件

除了将日志打印到控制台,你还可以通过向 logging.basicConfig() 传递 filename 关键字参数,将它们保存到文件中。

让我们来练习一下。

  1. 打开文件 configure_logging.py。
  2. 添加以下代码。它配置日志记录,将所有级别为 DEBUG 及以上的消息写入名为 myProgramLog.txt 的文件。
import logging

## Configure logging to write to a file
logging.basicConfig(filename='myProgramLog.txt', level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')

logging.debug('This is a debug message.')
logging.info('This is an info message.')
logging.warning('This is a warning message.')
  1. 在终端中运行脚本。
python3 configure_logging.py

这次,你不会在终端中看到任何输出。

  1. 检查 myProgramLog.txt 文件的内容。
cat myProgramLog.txt

你将看到所有日志消息都已写入文件,并附带了时间戳和严重性级别。

2023-10-27 10:30:00,123 - DEBUG - This is a debug message.
2023-10-27 10:30:00,123 - INFO - This is an info message.
2023-10-27 10:30:00,123 - WARNING - This is a warning message.

这结束了我们对 Python 基本调试技术的介绍。

总结

在这个实验(Lab)中,你学习并实践了几种基本的 Python 调试技术。你首先使用了 raise 语句来创建和触发自定义异常(exception),并使用 try...except 块来处理它们。然后,你学习了如何使用 traceback.format_exc() 将完整的异常回溯(traceback)捕获为字符串,以用于日志记录。

接下来,你探索了 assert 语句,它作为一种执行合理性检查(sanity checks)的方式,以便在程序进入意外状态时“快速失败”(fail fast)。最后,你深入研究了 logging 模块,学习了如何配置它来显示消息、使用不同的日志级别控制输出,以及将日志写入文件以进行持久化记录。

这些技能对于编写健壮、可维护且易于调试的 Python 应用程序至关重要。