8. 错误和异常

优质
小牛编辑
127浏览
2023-12-01

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: integer division or modulo 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: cannot concatenate 'str' and 'int' objects

最后一行的错误消息指示发生了什么事。异常有不同的类型,其类型会作为消息的一部分打印出来:在这个例子中的类型有ZeroDivisionErrorNameErrorTypeError。打印出来的异常类型的字符串就是内置的异常的名称。这对于所有内置的异常是正确的,但是对于用户自定义的异常就不一定了(尽管这是非常有用的惯例)。标准异常的名称都是内置的标识符(不是保留的关键字)。

这一行最后一部分给出了异常的详细信息和引起异常的原因。

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

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

8.3. 处理异常

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

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

Try语句按以下方式工作。

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

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

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

注意,此元组周围的括号是必需的,因为exceptValueError,e:是旧式的写法,在现代 Python 中通常写成 exceptValueErrorase: (如下所述)。为了保持向后兼容性,旧式语法仍然是支持的。这意味着exceptRuntimeError,TypeError不等同于except(RuntimeError,TypeError): 而等同于exceptRuntimeErrorasTypeError: , 这应该不是你想要的。

最后一个 except 子句可以省略异常名称,以当作通配符使用。使用这种方式要特别小心,因为它会隐藏一个真实的程序错误!它还可以用来打印一条错误消息,然后重新引发异常 (让调用者也去处理这个异常):

import sys

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except IOError as e:
    print "I/O error({0}): {1}".format(e.errno, e.strerror)
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 子句可以在异常名(或元组)之后指定一个变量。这个变量将绑定于一个异常实例,同时异常的参数将存放在实例的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
...    x, y = inst.args
...    print 'x =', x
...    print 'y =', y
...
<type 'exceptions.Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs

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

异常处理程序不仅处理直接发生在 try 子句中的异常,而且还处理 try 子句中调用的函数(甚至间接调用的函数)引发的异常。例如:

>>> def this_fails():
...     x = 1/0
...
>>> try:
...     this_fails()
... except ZeroDivisionError as detail:
...     print 'Handling run-time error:', detail
...
Handling run-time error: integer 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 MyError(Exception):
...     def __init__(self, value):
...         self.value = value
...     def __str__(self):
...         return repr(self.value)
...
>>> try:
...     raise MyError(2*2)
... except MyError as e:
...     print 'My exception occurred, value:', e.value
...
My exception occurred, value: 4
>>> raise MyError('oops!')
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
__main__.MyError: 'oops!'

在此示例中,Exception默认的init()被覆盖了。新的行为简单地创建了value 属性。这将替换默认的创建args 属性的行为。

异常类可以像其他类一样做任何事情,但是通常都会比较简单,只提供一些属性以允许异常处理程序获取错误相关的信息。创建一个能够引发几种不同错误的模块时,一个通常的做法是为该模块定义的异常创建一个基类,然后基于这个基类为不同的错误情况创建特定的子类:

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

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

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

    def __init__(self, expr, msg):
        self.expr = expr
        self.msg = msg

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

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

    def __init__(self, prev, next, msg):
        self.prev = prev
        self.next = next
        self.msg = msg

大多数异常的名字都以"Error"结尾,类似于标准异常的命名。

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

8.6. 定义清理操作

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

>>> try:
...     raise KeyboardInterrupt
... finally:
...     print 'Goodbye, world!'
...
Goodbye, world!
KeyboardInterrupt

不管有没有发生异常,在离开try语句之前总是会执行finally 子句。当try子句中发生了一个异常,并且没有except字句处理(或者异常发生在try或else子句中),在执行完finally子句后将重新引发这个异常。try语句由于break、contine或return语句离开时,同样会执行finally子句。以下是一个更复杂些的例子 (同时有except和finally字句的try语句的工作方式与 Python 2.5 一样):

>>> 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
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,

这段代码的问题就是代码执行完之后它还会让文件在一段不确定的时间内保持打开状态。这在简单的脚本中没什么,但是在大型应用程序中可能是一个问题。With语句可以确保像文件这样的对象总能及时准确地被清理掉。

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

执行该语句后,文件f 将始终被关闭,即使在处理某一行时遇到了问题。其它对象是否提供了预定义的清理行为要查看它们的文档。