异常处理与日志记录

PythonPythonBeginner
立即练习

This tutorial is from open-source community. Access the source code

💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版

简介

在这个实验中,你将学习如何在 Python 中实现异常处理。你将了解如何使用日志记录模块(logging module)进行更好的错误报告,这对于调试和维护你的代码至关重要。

你还将练习修改 reader.py 文件,以优雅地处理错误,而不是让程序崩溃。这种实践经验将提升你编写健壮 Python 程序的能力。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/ControlFlowGroup(["Control Flow"]) python(("Python")) -.-> python/ModulesandPackagesGroup(["Modules and Packages"]) python(("Python")) -.-> python/ErrorandExceptionHandlingGroup(["Error and Exception Handling"]) python/ControlFlowGroup -.-> python/conditional_statements("Conditional Statements") python/ControlFlowGroup -.-> python/for_loops("For Loops") python/ModulesandPackagesGroup -.-> python/standard_libraries("Common Standard Libraries") python/ErrorandExceptionHandlingGroup -.-> python/catching_exceptions("Catching Exceptions") python/ErrorandExceptionHandlingGroup -.-> python/raising_exceptions("Raising Exceptions") subgraph Lab Skills python/conditional_statements -.-> lab-132507{{"异常处理与日志记录"}} python/for_loops -.-> lab-132507{{"异常处理与日志记录"}} python/standard_libraries -.-> lab-132507{{"异常处理与日志记录"}} python/catching_exceptions -.-> lab-132507{{"异常处理与日志记录"}} python/raising_exceptions -.-> lab-132507{{"异常处理与日志记录"}} end

理解 Python 中的异常

在这一步中,你将学习 Python 中的异常。异常是编程中的一个重要概念,它能帮助我们处理程序运行时可能出现的意外情况。你还将了解当前代码在处理无效数据时崩溃的原因。理解这些将有助于你编写更健壮、更可靠的 Python 程序。

什么是异常?

在 Python 中,异常是程序执行过程中发生的事件,它会打乱程序的正常指令流。你可以把它想象成高速公路上的路障。一切顺利时,程序会按既定路径执行,就像汽车在畅通的道路上行驶。但当出现错误时,Python 会创建一个异常对象。这个对象就像一份报告,包含了出错信息,比如错误类型和代码中出错的位置。

如果这些异常没有得到妥善处理,程序就会崩溃。程序崩溃时,Python 会显示一个回溯信息(traceback message)。这个信息就像一张地图,能精确显示代码中出错的位置,对调试非常有用。

查看当前代码

首先,我们来看看 reader.py 文件的结构。这个文件包含了用于读取和转换 CSV 数据的函数。要在编辑器中打开这个文件,我们需要导航到正确的目录。在终端中使用 cd 命令:

cd /home/labex/project

现在我们已经进入了正确的目录,来看看 reader.py 的内容。这个文件有几个重要的函数:

  1. convert_csv():该函数接收数据行,并使用提供的转换器函数对其进行转换。它就像一台机器,将原材料(数据行)按照特定的配方(转换器函数)转化为另一种形式。
  2. csv_as_dicts():该函数读取 CSV 数据并将其转换为字典列表。它还会进行类型转换,确保字典中的每个数据项都是正确的类型,如字符串、整数或浮点数。
  3. read_csv_as_dicts():这是一个包装函数。它就像一个管理者,调用 csv_as_dicts() 函数来完成任务。

演示问题

让我们看看代码处理无效数据时会发生什么。我们将打开一个 Python 解释器,它就像一个游乐场,你可以在其中交互式地测试 Python 代码。在终端中使用以下命令打开 Python 解释器:

python3

Python 解释器打开后,我们将尝试读取 missing.csv 文件。这个文件包含一些缺失或无效的数据。我们将使用 reader.py 文件中的 read_csv_as_dicts() 函数来读取数据。

from reader import read_csv_as_dicts
port = read_csv_as_dicts('missing.csv', types=[str, int, float])

运行这段代码时,你会看到类似这样的错误信息:

Traceback (most recent call last):
  ...
ValueError: invalid literal for int() with base 10: ''

这个错误的出现是因为代码试图将空字符串转换为整数。空字符串不能表示有效的整数,所以 Python 无法完成转换。函数在遇到第一个错误时就会崩溃,并且停止处理文件中其余的有效数据。

要退出 Python 解释器,请输入以下命令:

exit()

理解错误流程

错误发生在 convert_csv() 函数中,具体是在以下这一行:

return list(map(lambda row: converter(headers, row), rows))

map() 函数将 converter 函数应用于 rows 列表中的每一行。converter 函数会尝试将类型(字符串、整数、浮点数)应用到每一行。但当遇到包含缺失数据的行时,它就会失败。map() 函数没有内置的异常处理机制,所以一旦出现异常,整个处理过程就会崩溃。

在下一个步骤中,你将修改代码以优雅地处理这些异常。这意味着程序不会崩溃,而是能够处理错误并继续处理其余的数据。

实现异常处理

在这一步中,我们将着重让你的代码变得更加健壮。当程序遇到无效数据时,通常会崩溃。但我们可以使用一种名为异常处理的技术来优雅地处理这些问题。你将修改 reader.py 文件以实现这一点。异常处理能让你的程序即使在遇到意外数据时也能继续运行,而不是突然停止。

理解 try-except 块

Python 提供了一种强大的方式来使用 try-except 块处理异常。下面我们来详细了解它们的工作原理。

try:
    ## Code that might cause an exception
    result = risky_operation()
except SomeExceptionType as e:
    ## Code that runs if the exception occurs
    handle_exception(e)

try 块中,你放置可能引发异常的代码。异常是程序执行过程中出现的错误。例如,如果你尝试将一个数除以零,Python 会引发一个 ZeroDivisionError 异常。当 try 块中出现异常时,Python 会停止执行 try 块中的代码,并跳转到匹配的 except 块。except 块包含处理异常的代码。SomeExceptionType 是你想要捕获的异常类型。你可以捕获特定类型的异常,也可以使用通用的 Exception 来捕获所有类型的异常。as e 部分允许你访问异常对象,该对象包含有关错误的信息。

修改代码

现在,让我们将所学的 try-except 块知识应用到 convert_csv() 函数中。在编辑器中打开 reader.py 文件。

  1. 将当前的 convert_csv() 函数替换为以下代码:
def convert_csv(rows, converter, header=True):
    """
    Convert a sequence of rows to an output sequence according to a conversion function.
    """
    if header:
        headers = next(rows)
    else:
        headers = []

    result = []
    for row_idx, row in enumerate(rows, start=1):
        try:
            ## Try to convert the row
            result.append(converter(headers, row))
        except Exception as e:
            ## Print a warning message for bad rows
            print(f"Row {row_idx}: Bad row: {row}")
            continue

    return result

在这个新的实现中:

  • 我们使用 for 循环而不是 map() 来处理每一行。这让我们对每一行的处理有更多的控制权。
  • 我们将转换代码包裹在 try-except 块中。这意味着如果在转换某一行时出现异常,程序不会崩溃,而是会跳转到 except 块。
  • except 块中,我们为无效行打印一条错误消息。这有助于我们识别哪些行有问题。
  • 打印错误消息后,我们使用 continue 语句跳过当前行并继续处理剩余的行。

完成这些更改后保存文件。

测试你的更改

让我们使用 missing.csv 文件来测试你修改后的代码。首先,在终端中运行以下命令打开 Python 解释器:

python3

进入 Python 解释器后,运行以下代码:

from reader import read_csv_as_dicts
port = read_csv_as_dicts('missing.csv', types=[str, int, float])
print(f"Number of valid rows processed: {len(port)}")

运行这段代码时,你应该会看到每一个有问题的行的错误消息。但程序会继续处理并返回有效的行。以下是你可能看到的输出示例:

Row 4: Bad row: ['C', '', '53.08']
Row 7: Bad row: ['DIS', '50', 'N/A']
Row 8: Bad row: ['GE', '', '37.23']
Row 13: Bad row: ['INTC', '', '21.84']
Row 17: Bad row: ['MCD', '', '51.11']
Row 19: Bad row: ['MO', '', '70.09']
Row 22: Bad row: ['PFE', '', '26.40']
Row 26: Bad row: ['VZ', '', '42.92']
Number of valid rows processed: 20

我们还来验证一下程序在处理有效数据时是否能正常工作。在 Python 解释器中运行以下代码:

valid_port = read_csv_as_dicts('valid.csv', types=[str, int, float])
print(f"Number of valid rows processed: {len(valid_port)}")

你应该会看到所有行都被无错误地处理。以下是输出示例:

Number of valid rows processed: 17

要退出 Python 解释器,运行以下命令:

exit()

现在你的代码更加健壮了。它可以通过跳过无效行来优雅地处理无效数据,而不是崩溃。这使得你的程序更加可靠和用户友好。

✨ 查看解决方案并练习

实现日志记录

在这一步中,我们将让你的代码更完善。不再使用简单的打印消息,而是使用 Python 的 logging 模块进行规范的日志记录。日志记录是跟踪程序运行情况的好方法,尤其是在处理错误和理解代码流程时。

理解日志记录模块

Python 中的 logging 模块为我们提供了一种灵活的方式,可从应用程序中输出日志消息。它比简单的打印语句强大得多。以下是它的功能:

  1. 不同的日志级别(DEBUG、INFO、WARNING、ERROR、CRITICAL):这些级别有助于我们对消息的重要性进行分类。例如,DEBUG 用于开发期间有用的详细信息,而 CRITICAL 用于可能导致程序停止的严重错误。
  2. 可配置的输出格式:我们可以决定日志消息的显示方式,例如添加时间戳或其他有用信息。
  3. 消息可定向到不同的输出(控制台、文件等):我们可以选择在控制台显示日志消息、将其保存到文件,甚至发送到远程服务器。
  4. 基于严重性的日志过滤:我们可以根据日志级别控制要查看的消息。

为 reader.py 添加日志记录

现在,让我们修改代码以使用日志记录模块。打开 reader.py 文件。

首先,我们需要导入 logging 模块,并为该模块设置一个日志记录器。在文件顶部添加以下代码:

import logging

## Set up a logger for this module
logger = logging.getLogger(__name__)

import logging 语句引入了 logging 模块,以便我们可以使用其功能。logging.getLogger(__name__) 为这个特定模块创建了一个日志记录器。使用 __name__ 可确保日志记录器具有与模块相关的唯一名称。

接下来,我们将修改 convert_csv() 函数,使用日志记录而不是打印语句。以下是更新后的代码:

def convert_csv(rows, converter, header=True):
    """
    Convert a sequence of rows to an output sequence according to a conversion function.
    """
    if header:
        headers = next(rows)
    else:
        headers = []

    result = []
    for row_idx, row in enumerate(rows, start=1):
        try:
            ## Try to convert the row
            result.append(converter(headers, row))
        except Exception as e:
            ## Log a warning message for bad rows
            logger.warning(f"Row {row_idx}: Bad row: {row}")
            ## Log the reason at debug level
            logger.debug(f"Row {row_idx}: Reason: {str(e)}")
            continue

    return result

这里的主要更改如下:

  • 我们将 print() 替换为 logger.warning() 来输出错误消息。这样,消息会以适当的警告级别进行记录,并且我们可以在以后控制其可见性。
  • 我们添加了一条新的 logger.debug() 消息,包含有关异常的详细信息。这为我们提供了更多关于出错原因的信息,但只有在日志级别设置为 DEBUG 或更低时才会显示。
  • str(e) 将异常转换为字符串,以便我们可以在日志消息中显示错误原因。

完成这些更改后,保存文件。

测试日志记录

让我们在启用日志记录的情况下测试你的代码。在终端中运行以下命令打开 Python 解释器:

python3

进入 Python 解释器后,执行以下代码:

import logging
import reader

## Configure logging level to see all messages
logging.basicConfig(level=logging.DEBUG)

port = reader.read_csv_as_dicts('missing.csv', types=[str, int, float])
print(f"Number of valid rows processed: {len(port)}")

在这里,我们首先导入 logging 模块和我们的 reader 模块。然后,使用 logging.basicConfig(level=logging.DEBUG) 将日志级别设置为 DEBUG。这意味着我们将看到所有日志消息,包括 DEBUG、INFO、WARNING、ERROR 和 CRITICAL。然后,我们调用 reader 模块中的 read_csv_as_dicts 函数,并打印处理的有效行数。

你应该会看到如下输出:

WARNING:reader:Row 4: Bad row: ['C', '', '53.08']
DEBUG:reader:Row 4: Reason: invalid literal for int() with base 10: ''
WARNING:reader:Row 7: Bad row: ['DIS', '50', 'N/A']
DEBUG:reader:Row 7: Reason: could not convert string to float: 'N/A'
...
Number of valid rows processed: 20

注意,日志记录模块会为每条消息添加一个前缀,显示日志级别(WARNING/DEBUG)和模块名称。

现在,让我们看看如果将日志级别更改为仅显示警告会发生什么。在 Python 解释器中运行以下代码:

## Reset the logging configuration
import logging
logging.basicConfig(level=logging.WARNING)

port = reader.read_csv_as_dicts('missing.csv', types=[str, int, float])

这次,我们使用 logging.basicConfig(level=logging.WARNING) 将日志级别设置为 WARNING。现在你只会看到 WARNING 消息,而 DEBUG 消息将被隐藏:

WARNING:reader:Row 4: Bad row: ['C', '', '53.08']
WARNING:reader:Row 7: Bad row: ['DIS', '50', 'N/A']
...

这显示了使用不同日志级别的优势。我们可以在不更改代码的情况下控制日志中显示的详细程度。

要退出 Python 解释器,运行以下命令:

exit()

恭喜!你现在已经在 Python 程序中实现了规范的异常处理和日志记录。这使你的代码更可靠,并在出现错误时为你提供更详细的信息。

总结

在这个实验中,你学习了 Python 中异常处理和日志记录的几个关键概念。首先,你掌握了在数据处理过程中异常是如何产生的,并实现了 try-except 块来优雅地处理它们。你还修改了代码,以便在出现错误时能继续处理有效数据。

其次,你了解了 Python 的日志记录模块及其相对于打印语句的优势。你实现了不同的日志级别,如 WARNING 和 DEBUG,并了解了如何为不同的详细程度配置日志记录。这些技能对于编写健壮的 Python 应用程序至关重要,特别是在处理易出错的外部数据、构建无人值守的应用程序或开发需要诊断信息的系统时。现在,你可以将这些技术应用到你的 Python 项目中,以提高其可靠性和可维护性。