8. 错误与异常

到目前为止,错误消息的提及还没有太多,但是如果您try了这些示例,则可能已经看到了一些。 (至少)有两种可区分的错误:语法错误异常

8.1. 语法错误

语法错误,也称为解析错误,可能是您还在学习 Python 时最常抱怨的一种:

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

解析器重复出现问题的行,并在检测到错误的行中的最早点处显示一点“箭头”。该错误是由标记(在箭头之前)引起(或至少在该标记之前)引起的:在此示例中,该错误是在函数print()处检测到的,因为之前没有冒号(':')。文件名和行号会打印出来,以便您知道在哪里 Importing 来自脚本的情况。

8.2. Exceptions

即使语句或表达式在语法上是正确的,但在try执行它时也可能导致错误。在执行过程中检测到的错误称为* exception *,它们并非无条件致命:您将很快学习如何在 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。打印为异常类型的字符串是发生的内置异常的名称。对于所有内置异常,这都是正确的,但对于用户定义的异常,则不必如此(尽管这是一个有用的约定)。标准异常名称是内置标识符(不是保留关键字)。

该行的其余部分根据异常的类型和引起异常的原因提供了详细信息。

错误消息的前面部分以堆栈回溯的形式显示了发生异常的上下文。通常,它包含一个列出源行的堆栈回溯。但是,它不会显示从标准 Importing 读取的行。

Built-in Exceptions列出了内置的异常及其含义。

8.3. 处理异常

可以编写处理选定异常的程序。请看下面的示例,该示例要求用户 Importing 直到 Importing 了有效的整数为止,但是允许用户break程序(使用 Control-C 或 os 支持的任何程序)。请注意,pass引发KeyboardInterrupt异常来表示用户生成的break。

>>> 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语句之后 continue 执行。

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

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

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

如果except子句中的类是同一类或其 Base Class,则该类与异常兼容(但不是相反的方式-列出派生类的 except 子句与 Base Class 不兼容)。例如,以下代码将按此 Sequences 打印 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 子句可以Ellipsis exception 名称,以用作通配符。请谨慎使用,因为这样很容易掩盖实际的编程错误!它也可以用于打印错误消息,然后重新引发异常(允许调用者也处理该异常):

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

tryexcept语句具有可选的* 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子句添加其他代码更好,因为它避免了意外捕获tryexcept语句保护的代码未引发的异常。

发生异常时,它可能具有关联的值,也称为异常的* 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

如果异常包含参数,则对于未处理的异常,它们将作为消息的最后部分(“详细信息”)打印。

如果异常处理程序立即在 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派生的类)。如果传递了异常类,则将pass不带参数的构造函数来隐式实例化该异常类:

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. 用户定义的异常

程序可以pass创建新的异常类来命名自己的异常(有关 python 类的更多信息,请参见Classes)。通常应直接或间接地从Exception类派生异常。

可以定义异常类,该异常类可以执行任何其他类可以做的任何事情,但是通常保持简单,通常仅提供许多属性,这些属性允许有关异常的信息由处理程序为异常处理程序提取。创建可能引发多个不同错误的模块时,通常的做法是为该模块定义的异常创建 Base Class,并为不同的错误条件创建特定的异常类的子类:

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

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

许多标准模块定义了自己的异常,以报告它们定义的Function中可能发生的错误。有关类的更多信息,请参见第Classes章。

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子句,则finally子句将作为try语句完成之前的最后一个任务执行。 finally子句运行,无论try语句是否产生异常。以下几点讨论了发生异常时更复杂的情况:

  • 如果在执行try子句期间发生异常,则可以由except子句处理该异常。如果except子句未处理该异常,则在执行finally子句后将重新引发该异常。

  • 执行exceptelse子句期间可能会发生异常。同样,在执行finally子句后,将重新引发异常。

  • 如果try语句到达breakcontinuereturn语句,则finally子句将在breakcontinuereturn语句执行之前执行。

  • 如果finally子句包含return语句,则返回的值将是finally子句的return语句中的值,而不是try子句的return语句中的值。

For example:

>>> def bool_return():
...     try:
...         return True
...     finally:
...         return False
...
>>> bool_return()
False

一个更复杂的例子:

>>> 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子句。pass分割两个字符串引发的TypeError不会由except子句处理,因此在执行finally子句后会重新引发。

在实际的应用程序中,finally子句对于释放外部资源(例如文件或网络连接)非常有用,无论该资源的使用是否成功。

8.7. sched 义的清理措施

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

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

此代码的问题在于,在这部分代码完成执行之后,它会在不确定的时间内使文件打开。在简单的脚本中这不是问题,但对于较大的应用程序可能是一个问题。 with语句允许使用诸如文件之类的对象,以确保始终及时,正确地清理它们。

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

执行该语句后,即使处理行时遇到问题,文件* f *也始终关闭。像文件一样提供 sched 义清除操作的对象将在其文档中进行说明。