Skip to main content

8. 错误和异常

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

8.1. 语法错误

语法错误,也称为解析错误,也许是你在学习Python时最常见的抱怨:

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

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

8.2. 例外

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

>>> 10 * (1/0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
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 关键字之间的语句)。

  • 如果没有发生异常,则跳过 除外 并且完成 try 语句的执行。

  • 如果在执行try子句期间发生异常,则跳过该子句的其余部分。然后如果它的类型匹配在 except 关键字之后命名的异常,则执行except子句,然后在 try 语句之后继续执行。

  • 如果发生与except子句中命名的异常不匹配的异常,则将异常传递给外部 try 语句;如果没有找到处理程序,它是一个 未处理的异常,执行停止与如上所示的消息。

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

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

except 子句中的类与异常是兼容的,如果它是相同的类或其基类(但不是相反的 - 一个except子句列出派生类与基类不兼容)。例如,以下代码将按该顺序打印B,C,D:

class B(Exception):
    pass

class C(B):
    pass

class D(C):
    pass

for cls in [B, C, D]:
    try:
        raise cls()
    except D:
        print("D")
    except C:
        print("C")
    except B:
        print("B")

注意,如果except子句颠倒(使用 except B 第一),它将打印B,B,B —第一个匹配的except子句被触发。

最后一个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子句,如果存在,必须遵循所有除外条款。如果try子句不引发异常,那么它对于必须执行的代码非常有用。例如:

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

使用 else 子句优于向 try 子句添加附加代码,因为它避免意外捕获未被由 try ... except 语句保护的代码引发的异常。

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

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

如果异常有参数,它们将作为未处理异常的消息的最后一部分(’detail’)。

异常处理程序不仅处理异常,如果它们立即在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: division by zero

8.4. 提高例外

raise 语句允许程序员强制发生指定的异常。例如:

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

raise 的唯一论据表明要提出的例外。这必须是异常实例或异常类(从 Exception 派生的类)。如果一个异常类被传递,它将被隐式实例化通过调用其构造函数没有参数:

raise ValueError  # shorthand for 'raise ValueError()'

如果需要确定是否引发了异常,但不打算处理它,则更简单的 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 <module>
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 <module>

finally子句 总是在离开 try 语句之前执行,无论是否发生异常。当 try 子句中发生异常并且没有被 except 子句处理(或者它发生在 exceptelse 子句中)时,它在 finally 子句被执行之后重新生成。当 try 语句的任何其他子句通过 breakcontinuereturn 语句离开时,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 <module>
  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 总是关闭,即使在处理行时遇到问题。像文件一样提供预定义清理操作的对象将在其文档中指出。