8. 错误和异常

未匹配的标注
本文档最新版为 3.8,旧版本可能放弃维护,推荐阅读最新版!

到目前为止,错误信息还未介绍,但你在学习前面示例的时候可能已经碰到一些错误信息。错误信息(至少)可以分为俩类:语法错误异常

8.1. 语法错误

语法错误,也叫作解析错误,这可能是你在学习Python的过程中,最容易碰到的错误:

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

解析结果显示出错的行代码,并用小箭头指明解析到错误的具体位置。
这个错误是在箭头所指向的位置:在本例中,错误是在这个函数中检测到的 print(),因为print函数之前应该存在的冒号缺失。文件名称和行号都被打印出来,这样你就可以知道这行有错误的代码是在哪个位置了。

8.2. 异常

一条语句或表达式即便是语法正确也有可能在运行的时候报错。程序执行过程中遇到的错误被称为异常。这种错误并不一定是致命错误,我们一会儿来看如何在程序中处理异常。大多数的异常并非由程序自身处理,而是会显示类似下面列举的错误信息。

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

错误信息的最后一行告诉我们程序遇到了什么类型的错误。异常有不同的类型,而其类型名称将会在错误信息中输出出来。上述样例中的异常类型依次是: ZeroDivisionErrorNameError 以及 TypeError 。错误信息中的异常类型是执行时抛出的内建异常类型。所有的内建类型都会如此,而虽然这是一个有用的惯例,但是用户自定义的异常并非一定如此。标准的异常类型是内建的标识符而非预留关键词。

这一行剩下的部分能告诉我们此处抛出异常的具体信息及触发的原因。

错误信息的前面部分能够通过栈调用信息告诉我们抛出异常时的上下文。一般情况下栈调用信息会包含函数调用所在的源代码行,但是不会包含从标准输入读入的信息。

Built-in Exceptions 列举了内建异常类型以及各自的含义。

8.3. 捕捉异常

Python 允许编程处理特定的异常。在下面这个例子中,程序要求用户进行输入,直到接收到一个合法的整数,同时也允许用户中断程序 (使用 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 子句 (位于 try 和 except 关键字之间的内容)。
  • 如果没有异常产生,则 except 子句被跳过,并且 try 声明的部分运行结束
  • 如果在执行 try 子句的过程中产生了一个异常,那么这个子句范围内产生异常位置之后的代码不会被执行,如果产生的异常位于 except 关键字后提到的若干异常之中,那么except 子句的内容将会接着 try 中刚被中止的位置继续执行。
  • 如果产生的异常并不在 exception 后面包含的若干异常中,该异常将会被抛给上一层的 try 语句;如果一个异常没有被任何一层 try-exception 语句捕捉,它就成为一个 未处理异常 ,程序执行将因此停止并显示如上所示的消息。
    一个 try 声明可以包含多个 except 子句来针对不同的异常做出不同的处理,但最多只有一个 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 子句, 当它出现时,必须在所有l except 子句之后。在执行 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 语句包含的异常。

当一个异常发生时,它可能有相关的值,或者是异常的 参数。尝试的有无和参数的类型取决于异常的类型。

except 子句可以在异常名之后指定变量。这些变量被绑定到一个异常实例中,实例中的参数储存在 instance.args 中。为了方便,异常实例定义了 __str__() ,所以参数可以被直接打印出来,而不需要引用 .args 。还可以在引发异常之前先实例化异常并根据需要添加任何属性。

>>> try:
...     raise Exception('spam', 'eggs')
... except Exception as inst:
...     print(type(inst))    # 异常实例
...     print(inst.args)     # 储存在 .args 中的参数
...     print(inst)          # __str__ 允许参数被直接打印,
...                          # 但是方法可能会被异常子类复写
...     x, y = inst.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  # '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. 用户定义的异常

程序可以通过创建一个新的异常类来命名其专属的异常 (参考 Classes 以获得更多有关Python类的信息)。 异常通常应该派生自 Exception 类,无论是直接地还是间接地。

异常类可以如通常类一样进行定义,但是通常会保持简洁,一般仅仅提供数个用于异常处理时可以解析异常信息的属性。在创建一个可以能引起多个不同异常的模块时,通常的做法是创建一个由模块定义的异常基类,并为不同的异常条件创建特定的异常子类:

class Error(Exception):
    """本模块的异常基类"""
    pass

class InputError(Error):
    """输入中错误引发的异常

    属性:
        expression -- 产生错误输入表达
        message -- 错误解释
    """

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

class TransitionError(Error):
    """尝试不允许状态转换的操作时引发的异常

    属性:
        previous -- 转换前的状态
        next -- 尝试转换的状态
        message -- 解释为什么特定的转换操作不允许
    """

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

大多数的异常定义都以「错误(Error)」结尾,和标准异常的命名类似。

很多标准模块定义了它们自己的异常,用于报告它们定义的函数中可能产生的错误。有关类的更多信息在章节 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 子句 总是在结束 try 语句之前执行,无论是否有异常产生。当异常产生在When an exception  try 子句中产生并未被 except 子句捕获(或异常在 except 或 else 子句中产生)时,异常将在 finally 子句被执行后再引发。 finally 子句也在「离开的路上」被执行,即当其他子句通过 breakcontinue or return 等语句离开 try 语句时。以下为一个更加复杂的例子:

>>> 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 总是被关闭,即使在处理每行时遇到错误。 与文件相同,提供预定义清理操作的对象将在其文档中指出这一点。

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
上一篇 下一篇
Summer
贡献者:1
讨论数量: 0
发起讨论 只看当前版本


暂无话题~