💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
### 8. 错误和异常 直到现在,我们还没有更多的提及错误信息,但是如果你真的尝试了前面的例子,也许你已经见到过一些。Python(至少)有两种错误很容易区分:*语法错误* 和*异常*。 ### 8.1. 语法错误 语法错误,或者称之为解析错误,可能是你在学习 Python 过程中最烦的一种: ~~~ >>> while True print('Hello world') File "<stdin>", line 1, in ? while True print('Hello world') ^ SyntaxError: invalid syntax ~~~ 语法分析器指出了出错的一行,并且在最先找到的错误的位置标记了一个小小的’箭头’。错误是由箭头*前面* 的标记引起的(至少检测到是这样的): 在这个例子中,检测到错误发生在函数[print()](# "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 ~~~ 最后一行的错误消息指示发生了什么事。异常有不同的类型,其类型会作为消息的一部分打印出来:在这个例子中的类型有[ZeroDivisionError](# "ZeroDivisionError")、 [NameError](# "NameError")和[TypeError](# "TypeError")。打印出来的异常类型的字符串就是内置的异常的名称。这对于所有内置的异常是正确的,但是对于用户自定义的异常就不一定了(尽管这是非常有用的惯例)。标准异常的名称都是内置的标识符(不是保留的关键字)。 这一行最后一部分给出了异常的详细信息和引起异常的原因。 错误信息的前面部分以堆栈回溯的形式显示了异常发生的上下文。通常调用栈里会包含源代码的行信息,但是来自标准输入的源码不会显示行信息。 [*内置的异常*](#) 列出了内置的异常以及它们的含义。 ### 8.3. 抛出异常 可以通过编程来选择处理部分异常。看一下下面的例子,它会一直要求用户输入直到输入一个合法的整数为止,但允许用户中断这个程序(使用Control-C或系统支持的任何方法);注意用户产生的中断引发的是 [KeyboardInterrupt](# "exceptions.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](#)语句之后的代码。 - 如果异常的类型与 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 子句可以在异常名之后指定一个变量。这个变量将绑定于一个异常实例,同时异常的参数将存放在实例的args中。为方便起见,异常实例定义了[__str__()](# "object.__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](# "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](# "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](# "Exception")默认的[__init__()](# "object.__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: 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 ~~~ 大多数异常的名字都以"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](#)子句。下面是一个更复杂些的例子: ~~~ >>> 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](# "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* 将始终被关闭,即使在处理某一行时遇到了问题。提供预定义的清理行为的对象,和文件一样,会在它们的文档里说明。