第 7 章:异常处理和调试

任何好的程序都利用了语言的异常处理机制。没有比让最终用户遇到软件问题并显示一个丑陋的错误消息,然后程序崩溃更令人沮丧的了。异常处理就是确保当程序遇到问题时,它将继续运行并向最终用户或程序管理员提供信息反馈。任何 Java 程序员从第一天起就熟悉异常处理,因为一些 Java 代码如果没有通过 try-catch-finally 语法进行某种形式的异常处理,甚至无法编译。Python 有类似于 Java 的结构,我们将在本章中讨论它们。

在找到异常之后,或者最好是在软件发布之前,您应该检查代码并进行调试,以找到并修复错误的代码。有许多不同的方法来调试和修复代码;我们将在本章中介绍一些调试方法。在 Python 和 Java 中,assert 关键字在这方面可以提供极大的帮助。我们将深入探讨 assert,并学习它可以用来帮助您节省调试那些难以发现的错误的时间的不同方法。

异常处理语法和与 Java 的区别

Java 开发人员非常熟悉 try-catch-finally 块,因为这是用于执行异常处理的主要机制。Python 异常处理与 Java 有些不同,但语法非常相似。但是,Java 在代码中抛出异常的方式略有不同。现在,请注意我刚刚使用了术语抛出……这是 Java 术语。Python 不会抛出异常,而是引发它们。两个不同的术语,基本上意味着相同的事情。在本节中,我们将逐步介绍在 Python 代码中处理和引发异常的过程,并向您展示它与 Java 中的不同之处。

对于那些不熟悉的人,我将向您展示如何在 Java 语言中执行一些异常处理。这将让您有机会比较两种语法并欣赏 Python 提供的灵活性。

try {
    // perform some tasks that may throw an exception
} catch (ExceptionType messageVariable) {
    // perform some exception handling
} finally {
    // execute code that must always be invoked
}

现在让我们继续学习如何在 Python 中实现这一点。我们不仅会看到如何处理和引发异常,而且您还将在本章后面学习一些其他很棒的技术。

捕获异常

你是否曾经在使用程序时,执行了某个操作导致程序崩溃并显示一个令人讨厌的错误消息?这种情况发生的频率比应该发生的频率要高,因为大多数异常都可以被捕获并以良好的方式处理。所谓良好的处理方式,是指程序不会崩溃,最终用户会收到一个描述性错误消息,说明问题是什么,在某些情况下还会说明如何解决问题。编程语言中的异常处理机制就是为此目的而开发的。

下面列出了 Python 语言中内置的所有异常,以及对每个异常的描述。你可以将这些异常中的任何一个写入一个子句并尝试处理它们。在本章的后面,我将向你展示如何处理它们,如果你愿意的话。最后,如果你想抛出一种不符合这些异常类型中的任何一种的特定类型的异常,那么你可以编写自己的异常类型对象。

异常 描述
BaseException 所有其他异常的根异常
GeneratorExit 由生成器的 close() 方法引发,用于终止迭代
KeyboardInterrupt 由中断键引发
SystemExit 程序退出
异常 所有非退出异常的根
StopIteration 引发以停止迭代操作
StandardError 所有内置异常的基类
ArithmeticError 所有算术异常的基类
FloatingPointError 当浮点运算失败时引发
OverflowError 算术运算太大
ZeroDivisoinError 除法或取模运算的除数为零
AssertionError 当断言语句失败时使用
AttributeError 属性引用或分配失败
EnvironmentError 在 Python 之外发生了错误
IOError 输入/输出操作中的错误
OSError os 模块中发生了错误
EOFError input() 或 raw_input() 尝试读取文件末尾之后的内容
ImportError 导入未能找到模块或名称
LookupError IndexError 和 KeyError 的基类
IndexError 序列索引超出范围
KeyError 引用了不存在的映射(字典)键
MemoryError 内存耗尽
NameError 未能找到局部或全局名称
UnboundLocalError 引用了未赋值的局部变量
ReferenceError 尝试访问已垃圾回收的对象
RuntimeError 过时的万能错误
NotImplementedError 当一个功能未实现时引发
SyntaxError 解析器遇到语法错误
IndentationError 解析器遇到缩进问题
TabError 制表符和空格的混合不正确
SystemError 非致命解释器错误
TypeError 向内置运算符或函数传递了不合适的类型
ValueError 参数错误,不属于 TypeError 或更精确的错误
Warning 所有警告的基类

try-except-finally 块用于 Python 程序中执行异常处理任务。与 Java 类似,可能引发异常或可能不引发异常的代码应该放在 try 块中。不同的是,可能被捕获的异常应该放在 except 块中,类似于 Java 的 catch 等价物。任何必须执行的任务,无论是否抛出异常,都应该放在 finally 块中。

try-except-finally 逻辑

try:
    # perform some task that may raise an exception
except Exception, value:
    # perform some exception handling
finally:
    # perform tasks that must always be completed

Python 还提供了一个可选的 else 子句,用于创建 try-except-else 逻辑。如果在块中没有发现异常,则运行放在 else 块中的这段可选代码。

try-finally 逻辑

try:
    # perform some tasks that may raise an exception
finally:
    # perform tasks that must always be completed

try-except-else 逻辑

try:
    # perform some tasks that may raise an exception
except:
    # perform some exception handling
else:
    # perform some tasks that should only be performed if no exceptions are thrown

你可以在 except 块中命名要捕获的特定类型的异常,或者你可以通过不命名任何异常来泛化地定义异常处理块。当然,最佳实践指出你应该始终尝试命名异常,然后为该情况提供最佳的处理解决方案。毕竟,如果程序只是简单地吐出一个令人讨厌的错误,那么异常处理块根本没有帮助解决问题。但是,在某些罕见的情况下,当我们只是希望忽略错误并继续执行时,不显式引用异常类型将是有利的。except 块还允许我们定义一个变量,异常消息将被分配给该变量。这使我们能够存储该消息并在异常处理代码块中的某个地方显示它。如果你从 Jython 中调用一段 Java 代码,而 Java 代码抛出了异常,那么它可以在 Jython 中以与 Jython 异常相同的方式进行处理。

示例 5-1:Python 中的异常处理

# Code without an exception handler
>>> x = 10
>>> z = x / y
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'y' is not defined

# The same code with an exception handling block
>>> x = 10
>>> try:
...     z = x / y
... except NameError, err:
...     print "One of the variables was undefined: ", err
...

One of the variables was undefined:  name 'y' is not defined

请注意用于定义保存错误消息的变量的语法。也就是说,Python 和 Jython 2.5 中的 *except ExceptionType, value* 语法与 2.5 之后的语法不同。在 Python 2.6 中,语法略有变化,以便为即将推出的 Python 3 做好准备,Python 3 将专门使用新的语法。虽然不打算偏离主题太远,但我认为有必要注意,这种语法将在 Jython 的未来版本中发生变化。

Jython 和 Python 2.5 及更早版本

try:
    // code
except ExceptionType, messageVar:
    // code

Jython 2.6(尚未实现)和 Python 2.6 及更高版本

try:
    // code
except ExceptionType as messageVar:
    // code

我们之前提到过,在编写异常处理代码时不显式命名异常类型是一种不好的编程习惯。这是真的,但是 Python 为我们提供了另一种方法来获取抛出的异常类型。*sys* 包中提供了一个名为 *sys.exc_info()* 的函数,它将为我们提供异常类型和异常消息。如果我们将一些代码包装在 *try-except* 块中,但我们不确定可能会抛出哪种类型的异常,这将非常有用。以下是用这种技术的示例。

示例 5-2:使用 sys.exc_info()

# Perform exception handling without explicitly naming the exception type
>>> x = 10
>>> try:
...     z = x / y
... except:
...     print "Unexpected error: ", sys.exc_info()[0], sys.exc_info()[1]
...
Unexpected error:  <type 'exceptions.NameError'> name 'y' is not defined

有时您可能会遇到需要捕获多个异常的情况。如果您需要进行此类异常处理,Python 提供了两种不同的选项。您可以使用多个 *except* 子句,这可以解决问题并运行良好,但可能会变得过于冗长。您拥有的另一个选项是在 *except* 语句中将异常类型括在括号中并用逗号分隔。请查看以下示例,该示例使用与 *示例 5-1* 中相同的示例来展示后一种方法。

示例 5-3:处理多个异常

# Catch NameError, but also a ZeroDivisionError in case a zero is used in the equation
>>> x = 10
>>> try:
...     z = x / y
... except (NameError,ZeroDivisionError),  err:
...     print "One of the variables was undefined: ", err
...
One of the variables was undefined:  name 'y' is not defined


# Using mulitple except clauses
>>> x = 10
>>> y = 0
>>> try:
...     z = x / y
... except NameError, err1:
...     print err1
... except ZeroDivisionError, err2:
...     print 'You cannot divide a number by zero!'
...
You cannot divide a number by zero!

*try-except* 块可以嵌套到您想要的任何深度。在嵌套异常处理块的情况下,如果抛出异常,程序控制将跳出接收错误的最内层块,并跳到其上方的块。这与您在嵌套循环中工作并遇到 *break* 语句时采取的操作非常相似,您的代码将停止执行并跳回到外层循环。以下示例显示了这种逻辑的示例。

示例 5-4:嵌套异常处理块

# Perform some division on numbers entered by keyboard
 try:
     # do some work
     try:
         x = raw_input ('Enter a number for the dividend:  ')
         y = raw_input('Enter a number to divisor: ')
         x = int(x)
         y = int(y)
     except ValueError, err2:
         # handle exception and move to outer try-except
         print 'You must enter a numeric value!'
     z = x / y
 except ZeroDivisionError, err1:
    # handle exception
     print 'You cannot divide by zero!'
 except TypeError, err3:
     print 'Retry and only use numeric values this time!'
 else:     print 'Your quotient is: %d' % (z)

引发异常

您经常会发现需要引发自己的异常。也许您期望某种类型的键盘输入,而用户输入了您的程序不喜欢的错误内容。这将是您想要引发自己的异常的情况。*raise* 语句可用于让您在认为合适的地方引发异常。使用 *raise* 语句,您可以导致任何 Python 异常类型被引发,您可以引发您定义的自己的异常(在下一节中讨论),或者您可以引发字符串异常。*raise* 语句类似于 Java 语言中的 *throw* 语句。在 Java 中,我们可能会选择在必要时抛出异常。但是,Java 还允许您在方法中使用 *throws* 子句来应用特定方法,如果可能在方法中抛出异常,而不是在方法中使用 try-catch 处理程序。Python 不允许您使用 *raise* 语句执行此类技术。

raise 语句语法

raise ExceptionType or String[, message[, traceback]]

从语法中可以看出,使用 *raise* 允许您变得更有创意,因为您可以在引发错误时使用自己的字符串。但是,这并不被视为最佳实践,因为您应该尽可能尝试引发已定义的异常类型。您还可以提供一条简短的消息来解释错误。此消息可以是任何字符串。最后,您可以通过使用 *sys.exc_info()* 来提供 *回溯*。现在您肯定已经看到 Python 解释器中引发的一些异常了。每次引发异常时,都会出现一条由解释器创建的消息,以向您提供有关异常以及可能出现问题的代码行的反馈。每次引发异常时,都会有一个 *回溯* 部分。这确实为您提供了有关异常引发位置的更多信息。

示例 5-5:使用 raise 语句

>>> raise TypeError,"This is a special message"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: This is a special message

定义您自己的异常

您可以在 Python 中通过创建异常类来定义自己的异常。现在,类是我们尚未涵盖的主题,因此本节内容略有超前,但它相当简单。您只需使用 *class* 关键字定义一个类,然后为它命名即可。异常类应该继承自基本异常类 *Exception*。最简单的定义异常可以在类中简单地使用 pass 语句。更复杂的异常类可以接受参数并定义初始化器。将您的异常命名为以 *Error* 为后缀也是一个好习惯。

示例 5-6:定义异常类

class MyNewError(Exception):
    pass

上面的示例是您可以创建的最简单的异常类型。现在,可以像其他任何异常一样引发上面创建的异常。

raise MyNewError, “Something happened in my program”

更复杂的异常类可以按如下方式编写。

示例 5-7:使用初始化器的异常类

class MegaError(Exception):
    “”” This is raised when there is a huge problem with my program”””
    def __init__(self, val):
        self.val = val
    def __str__(self):
        return repr(self.val)

发出警告

可以在程序中的任何时间发出警告,并且可以用来显示某种类型的警告消息,但它们不一定导致执行中止。一个很好的例子是,当您希望弃用某个方法或实现,但仍使其可用于兼容性时。您可以创建一个警告来提醒用户,让他们知道这些方法已弃用,并将其指向新的定义,但程序不会中止。警告很容易定义,但如果您希望使用过滤器定义有关它们的规则,它们可能会很复杂。与异常类似,有许多定义的警告可用于分类。为了使这些警告能够轻松地转换为异常,它们都是 *Exception* 类型的实例。

表 5-2. Python 警告类别

Warning 描述
Warning 根警告类
UserWarning 用户定义的警告
DeprecationWarning 警告使用已弃用的功能
SyntaxWarning 语法问题
RuntimeWarning 运行时问题
FutureWarning 警告某个特定功能将在未来版本中更改

表 5-1:异常

要发出警告,您必须首先将 *warnings* 模块导入到您的程序中。完成此操作后,只需调用 *warnings.warn()* 函数并向其传递包含警告消息的字符串即可。但是,如果您想控制发出的警告类型,您也可以传递警告类别。

import warnings
…
warnings.warn(“this feature will be deprecated”)
warnings.warn(“this is a more involved warning”, RuntimeWarning)

将 warnings 模块导入到您的代码中,您可以访问许多内置的警告函数,这些函数可以被使用。如果您想过滤警告并更改其行为,您可以通过创建过滤器来做到这一点。以下是 *warnings* 模块附带的函数列表。

函数和描述  
warn(message[, category[, stacklevel]]) 发出警告。参数包括消息字符串、可选的警告类别以及可选的 stacklevel,它告诉警告应来自哪个堆栈帧。
warn_explicit(message, category, filename, lineno[, module[, registry]]) 这提供了一个更详细的警告消息,并将类别设置为必填参数。filename、lineno 和 module 指示警告的位置。registry 代表所有当前活动的警告过滤器。
showwarning(message, category, filename, lineno[, file]) 使您能够将警告写入文件。
formatwarning(message, category, filename, lineno) 创建一个表示警告的格式化字符串。
resetwarnings() 重置所有警告过滤器。
filterwarnings(action[, message[, category[, module[, lineno[, append]]]]])  

这将一个条目添加到警告过滤器列表中。警告过滤器允许您修改警告的行为。警告过滤器中的操作可以是以下操作表中的一个,message 是一个正则表达式,category 是要发出的警告类型,module 可以是正则表达式,lineno 是要与所有行匹配的行号,append 指定过滤器是否应附加到所有过滤器的列表中。

过滤器操作 描述
‘always’ 始终打印警告消息
‘default’ 为每个出现警告的位置打印一次警告
‘error’ 将警告转换为异常
‘ignore’ 忽略警告
‘module’ 为每个出现警告的模块打印一次警告
‘一次’ 仅打印警告一次

表 5-3. 警告函数

警告过滤器用于修改特定警告的行为。可以使用许多不同的警告过滤器,并且每次调用filterwarnings()函数都会在需要时将另一个警告追加到过滤器列表中。要查看当前正在使用的过滤器,请发出命令print warnings.filters。还可以通过使用 –W 选项从命令行指定警告过滤器。最后,可以使用resetwarnings()函数将所有警告重置为默认值。

-Waction:message:category:module:lineno

断言和调试

通过使用assert语句和__debug__变量,调试在 Python 中可以是一项轻松的任务。断言是可以在代码中打印以指示特定代码段的行为与预期不符的语句。断言检查表达式的真假值,如果为假,则会发出AssertionError以及可选的消息。如果表达式计算结果为真,则断言将完全被忽略。

assert expression [, message]

通过在整个程序中有效地使用assert语句,您可以轻松地捕获可能发生的任何错误,并使调试生活变得更加轻松。以下示例将向您展示assert语句的使用方法。

#  The following example shows how assertions are evaluated
>>> x = 5
>>> y = 10
>>> assert x < y, "The assertion is ignored"
>>> assert x > y, "The assertion works"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError: The assertion works

您可以通过将应仅在调试目的运行的整个代码块放在基于变量值的条件语句中来使用内部__debug__变量。

示例 5-10:使用 __debug__

if __debug__:
    # perform some debugging tasks

上下文管理器

确保代码编写正确以管理资源(如文件或数据库连接)是一个重要主题。如果文件或数据库连接被打开但从未关闭,则我们的程序可能会遇到问题。通常,开发人员选择使用问题。通常,开发人员选择使用try-finally块来确保这些资源得到妥善处理。虽然这是一种可接受的资源管理方法,但有时可能会被误用,并在程序中引发异常时导致问题。例如,如果我们正在使用数据库连接,并且在打开连接后发生异常,则程序控制可能会跳出当前块并跳过所有后续处理。在这种情况下,连接可能永远不会关闭。这就是上下文管理的概念成为 Jython 中一项重要的新功能的地方。通过使用with语句进行上下文管理是 Jython 2.5 的新功能,它是一种确保资源按预期管理的非常好的方法。

要使用with语句,您必须从 __future__ 中导入。with语句基本上允许您获取一个对象并使用它,而无需担心资源管理。例如,假设我们想在系统上打开一个文件并从中读取一些行。要执行文件操作,您首先需要打开文件,执行任何处理或读取文件内容,然后关闭文件以释放资源。使用with语句进行上下文管理允许您简单地打开文件并在简洁的语法中使用它。

示例 5-11:Python with 语句示例

#  Read from a text file named players.txt
>>> from __future__ import with_statement
>>> with open('players.txt','r') as file:
...     x = file.read()
...
>>> print x
This is read from the file

在上面的示例中,我们没有担心关闭文件,因为上下文为我们处理了这个问题。这适用于扩展上下文管理协议的对象。换句话说,任何实现名为__enter__()__exit__()的两个方法的对象都符合上下文管理协议。当with语句开始时,将执行__enter__()方法。同样,作为with语句结束时执行的最后一个操作,将执行__exit__()方法。__enter__()方法不接受任何参数,而__exit__()方法接受三个可选参数type、valuetraceback__exit__()方法返回TrueFalse值以指示是否抛出了异常。with语句上的as variable子句是可选的,因为它将允许您在代码块中使用该对象。如果您正在使用资源(如锁),那么您可能不需要可选子句。

如果您遵循上下文管理协议,则可以创建可以使用此技术自己的对象。__enter__()方法应该创建您正在尝试工作的任何对象(如果需要)。如果您正在使用不可变对象,那么您需要在__enter__()方法中创建该对象的副本以进行操作。另一方面,__exit__()方法可以简单地返回False,除非需要进行其他类型的清理处理。

摘要

本章讨论了 Python 应用程序中异常和异常处理的许多不同主题。首先,您学习了try-except-finally代码块的异常处理语法以及它的使用方法。然后,我们讨论了为什么有时可能需要引发您自己的异常以及如何做到这一点。该主题导致了如何定义异常的讨论,我们了解到为了做到这一点,我们必须定义一个扩展Exception类型对象的类。

在了解了异常之后,我们深入研究了警告框架并讨论了如何使用它。在代码可能已弃用并且您想警告用户但不想引发任何异常的情况下,使用警告可能很重要。该主题之后是断言以及如何使用断言语句来帮助我们调试程序。最后,我们触及了上下文管理器和使用 Jython 2.5 中的新with语句的主题。

在下一章中,您将深入研究创建类并学习 Python 中的面向对象编程。希望如果本章或本书之前讨论的主题由于不熟悉面向对象而可能不清楚,它们将在第 6 章中得到澄清。