8. 错误和异常

直到现在错误消息没有提到过,但如果你已经尝试了你可能看到一些例子。Python(至少)有两种错误很容易区分:语法错误异常

8.1. 语法错误

语法错误(也称为解析错误)可能是您仍在学习Python时最常见的抱怨:

>>> while True print('Hello world')
  File "<stdin>", line 1, in ?
    while True print('Hello world')
                   ^
SyntaxError: invalid syntax

解析器重复违规行并显示指向检测到错误的行中最早点的一个“箭头”。错误是由箭头前面的标记引起的(至少检测到是这样的): 在这个例子中,检测到错误发生在函数 print(),因为在它之前缺少一个冒号(':'打印文件名和行号,以便在输入来自脚本时知道在哪里查看。

8.2. 异常

即使语句或表达式在语法上正确,当尝试执行语句或表达式时也可能导致错误。执行期间检测到的错误称为异常,并不是无条件致命的:您很快将学习如何在Python程序中处理它们。然而,大多数异常不由程序处理,并导致错误消息,如下所示:

>>> 10 * (1/0)
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: Can't convert 'int' object to str implicitly

错误消息的最后一行指示发生了什么。异常有不同的类型,其类型会作为消息的一部分打印出来:在这个例子中的类型有ZeroDivisionErrorNameErrorTypeError.打印为异常类型的字符串是发生的内建异常的名称。这对于所有内建异常都是如此,但是对于用户定义的异常不需要是真的(虽然它是一个有用的约定)。标准异常名称是内部标识符(不是保留的关键字)。

该行的其余部分基于异常的类型及其引起的原因提供详细信息。

错误消息的前面部分以堆栈跟踪的形式显示发生异常的上下文。通常调用栈里会包含源代码的行信息,但是来自标准输入的源码不会显示行信息。

内置的异常 列出了内置的异常以及它们的含义。

8.3. 抛出异常

可以编写处理所选异常的程序。看一下下面的例子,它会一直要求用户输入直到输入一个合法的整数为止,但允许用户中断这个程序(使用 Control-C 或系统支持的任何方法);注意用户产生的中断引发的是 KeyboardInterrupt 异常。

>>> while True:
...     try:
...         x = int(input("Please enter a number: "))
...         break
...     except ValueError:
...         print("Oops!  That was no valid number.  Try again...")
...

try 语句按以下方式工作。

  • 首先,执行 try 子句tryexcept 关键字之间的语句)。
  • 如果未发生任何异常,忽略 except 子句try 语句执行完毕。
  • 如果在执行try子句期间发生异常,则跳过该子句的其余部分。如果异常的类型与 except 关键字后面的异常名匹配, 则执行 except 子句,然后继续执行 try 语句之后的代码。
  • 如果异常的类型与 except 关键字后面的异常名不匹配,它将被传递给上层的 try 语句;如果没有找到处理这个异常的代码,它就成为一个 未处理异常 ,程序会终止运行并显示一条如上所示的信息。

try 语句可能有多个子句,以指定不同的异常处理程序。最多只有一个处理程序将被执行。处理程序只处理发生在相应 try 子句中的异常,不会处理同一个 try 子句的其他处理程序中发生的异常。except子句可以将多个异常命名为括号元组,例如:

... except (RuntimeError, TypeError, NameError):
...     pass

最后一个except子句可以省略异常名称,作为通配符。使用这个非常小心,以这种方式很容易掩盖真正的编程错误!它也可以用于打印错误消息,然后重新提出异常(允许调用者处理异常):

import sys

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except OSError as err:
    print("OS error: {0}".format(err))
except ValueError:
    print("Could not convert data to an integer.")
except:
    print("Unexpected error:", sys.exc_info()[0])
    raise

try ...except 语句有一个可选的 else 子句 ,其出现时,必须放在所有 except 子句的后面。如果try子句不引发异常,那么它对于必须执行的代码非常有用。例如:

for arg in sys.argv[1:]:
    try:
        f = open(arg, 'r')
    except IOError:
        print('cannot open', arg)
    else:
        print(arg, 'has', len(f.readlines()), 'lines')
        f.close()

使用 else 子句比把额外的代码放在 try 子句中要好,因为它可以避免意外捕获不是由 try ... except 语句保护的代码所引发的异常。

当异常发生时,它可能有一个关联的值,也称为异常的参数参数的存在和类型取决于异常类型。

except子句可以在异常名称之后指定一个变量。这个变量将绑定于一个异常实例,同时异常的参数将存放在 instance.args 中。为方便起见,异常实例定义了 __str__() ,因此异常的参数可以直接打印而不必引用 .args还可以在提升之前首先实例化异常并且根据需要向其添加任何属性。

>>> try:
...     raise Exception('spam', 'eggs')
... except Exception as inst:
...     print(type(inst))    # the exception instance
...     print(inst.args)     # arguments stored in .args
...     print(inst)          # __str__ allows args to be printed directly,
...                          # but may be overridden in exception subclasses
...     x, y = inst.args     # unpack args
...     print('x =', x)
...     print('y =', y)
...
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs

对于未处理的异常,如果它含有参数,那么参数会作为异常信息的最后一部分打印出来。

异常处理程序不仅能处理在try子句直接发生的异常,也能处理try子句调用的函数中(即使是间接地)发生的异常。例如:

>>> def this_fails():
...     x = 1/0
...
>>> try:
...     this_fails()
... except ZeroDivisionError as err:
...     print('Handling run-time error:', err)
...
Handling run-time error: int division or modulo by zero

8.4. 引发异常

raise 语句允许程序员强行引发一个指定的异常。例如:

>>> raise NameError('HiThere')
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
NameError: HiThere

raise 的唯一参数指示要引发的异常。它必须是一个异常实例或异常类(从 Exception 派生的类)。

如果你确定需要引发异常,但不打算处理它,一个简单形式的 raise 语句允许你重新引发异常:

>>> try:
...     raise NameError('HiThere')
... except NameError:
...     print('An exception flew by!')
...     raise
...
An exception flew by!
Traceback (most recent call last):
  File "<stdin>", line 2, in ?
NameError: HiThere

8.5. 用户定义的异常

程序可以通过创建新的异常类来命名自己的异常(Python 类的更多内容请参见 )。异常通常应该继承 Exception 类,直接继承或者间接继承都可以。

异常类可以被定义为任何其他类可以做的任何事情,但通常保持简单,通常只提供一些属性,允许处理程序为异常提取错误的信息。当创建一个可以引入几个不同的错误的模块时,一个常见的做法是为该模块定义的异常创建一个基类,并为不同的错误条件创建特定的异常类的子类:

class Error(Exception):
    """Base class for exceptions in this module."""
    pass

class InputError(Error):
    """Exception raised for errors in the input.

    Attributes:
        expression -- input expression in which the error occurred
        message -- explanation of the error
    """

    def __init__(self, expression, message):
        self.expression = expression
        self.message = message

class TransitionError(Error):
    """Raised when an operation attempts a state transition that's not
    allowed.

    Attributes:
        previous -- state at beginning of transition
        next -- attempted new state
        message -- explanation of why the specific transition is not allowed
    """

    def __init__(self, previous, next, message):
        self.previous = previous
        self.next = next
        self.message = message

大多数异常都以“错误”结尾的名称定义,类似于标准异常的命名。

许多标准模块定义自己的异常来报告可能在它们定义的函数中出现的错误。 这一章给出了类的详细信息。

8.6. 定义清理操作

try 语句有另一个可选的子句,目的在于定义必须在所有情况下执行的清理操作。例如:

>>> try:
...     raise KeyboardInterrupt
... finally:
...     print('Goodbye, world!')
...
Goodbye, world!
KeyboardInterrupt
Traceback (most recent call last):
  File "<stdin>", line 2, in ?

不管有没有发生异常,在离开 try 语句之前总是会执行 finally 子句。try 子句中发生了一个异常,并且没有 except 字句处理(或者异常发生在 exceptelse 子句中),在执行完 finally 子句后将重新引发这个异常。try 语句由于 breakcontinereturn 语句离开时,同样会执行finally 子句。一个更复杂的例子:

>>> def divide(x, y):
...     try:
...         result = x / y
...     except ZeroDivisionError:
...         print("division by zero!")
...     else:
...         print("result is", result)
...     finally:
...         print("executing finally clause")
...
>>> divide(2, 1)
result is 2.0
executing finally clause
>>> divide(2, 0)
division by zero!
executing finally clause
>>> divide("2", "1")
executing finally clause
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<stdin>", line 3, in divide
TypeError: unsupported operand type(s) for /: 'str' and 'str'

正如您所看到的,在任何情况下都会执行 finally 子句。由两个字符串相除引发的 TypeError 异常没有被 except子句处理,因此在执行 finally 子句后被重新引发。

在真实的应用程序中, finally 子句用于释放外部资源(例如文件或网络连接),不管资源的使用是否成功。

8.7. 预定义清理操作

某些对象定义在不再需要对象时执行的标准清除操作,而不管使用该对象的操作是成功还是失败。查看下面的示例,尝试打开一个文件并将其内容打印到屏幕上。

for line in open("myfile.txt"):
    print(line, end="")

此代码的问题是,在代码的此部分完成执行后,它使文件打开一段不确定的时间量。这不是简单脚本中的问题,但对于较大的应用程序可能是一个问题。with 语句可以确保像文件这样的对象总能及时准确地被清理掉。

with open("myfile.txt") as f:
    for line in f:
        print(line, end="")

执行该语句后,文件 f 将始终被关闭,即使在处理某一行时遇到了问题。像文件一样提供预定义清除操作的对象将在其文档中指出。