8.错误和异常
8.1 常见报错
程序中经常会出错,常见的错误包括但不限于:
- 语法错误:“SyntaxError:invalid syntax”
- 异常:xxError,如NameError、TypeError、IndentationError、ModuleNotFoundError等
语法错误,在运行前就可以发现。如果使用PyCharm会有红色波浪线提醒你,请检查拼写、缩进、符号等是否符合语法。(SyntaxError也是一种异常,但是因为它比较特殊,在运行前就可以检查出来,所以单独说。)
异常情况很多,需要根据报错内容具体分析。下面我们看看异常到底是什么以及如何处理异常。
8.2 异常
程序执行时往往会出现预期之外的错误,也就是异常。
这些错误未必是程序设计的问题,也可能是用户非法输入、网络问题等导致程序出错。
例如一个计算器程序,用户输入1/0的时候,0作分母是无意义的。因此程序无法正常执行,引发报错。
>>> 10 * (1/0) Traceback (most recent call last): File "<stdin>", line 1, in <module> ZeroDivisionError: division by zero
实际程序中,我们可能遇到各种异常。
内置异常 — Python 3.10.4 文档里提供了大多数可能的异常,如IO异常,迭代异常、编码错误异常等等。
BaseException是所有异常的基类,它可以用来捕获所有异常。
但更常用是Exception。Exception是所有内置的非系统退出类异常的基类。 所有用户自定义异常也应当派生自此类。
8.3 处理异常
8.3.1 try-except
一般用try-except 语句来提前预防错误。
语法格式:
try: ... 执行一些可能出错的操作 except 异常类型: ... 对出错进行一个说明和处理
例如,我们写了一个从用户输入读取a,b,并计算a/b的程序。
用户可能输入一个非数字内容,引发ValueError
,也可能输入0作为除数,引发ZeroDivisionError
。
于是我们把可能出错的语句放在try
里面,并且用 except
捕捉错误。
try: a = int(input('a= ')) b = int(input('b= ')) print('a/b= ',a/b) except (ValueError,ZeroDivisionError): print("无效输入,请重试")
try
语句的工作原理如下:
- 首先,执行 try 子句 。
- 如果没有触发异常,则跳过 except 子句,
try
语句执行完毕。 - 如果在执行 try 子句时发生了异常,则跳过该子句中剩下的部分。 如果异常的类型与 except 关键字后指定的异常相匹配,则会执行 except 子句,然后跳到 try/except 代码块之后继续执行。如果发生的异常与 except 子句 中指定的异常不匹配,则它会被传递到外部的 try 语句中;如果没有找到处理程序,则它是一个 未处理异常 且执行将终止并输出报错信息。except 子句 可以用带圆括号的元组来指定多个异常,例如:
except (RuntimeError, TypeError, NameError): pass
try
后面可以接多个except
,来捕获多种异常。如果异常被前面的except捕获了,则后面的except不会继续执行:
import sys try: f = open('myfile.txt') s = f.readline() i = int(s.strip()) except OSError as err: print(err.args) print("OS error: {0}".format(err)) except ValueError: print("Could not convert data to an integer.") except BaseException as err: print(f"Unexpected {err=}, {type(err)=}") raise
except 子句 可以在异常名称后面用as
指定一个变量。 这个变量会绑定到一个异常实例并将参数存储在 instance.args
中。 print(err)会调用异常类的__str__()
方法,获取表示异常的字符串。
8.3.2 try-except-else
try
… except
语句具有可选的 else 子句,该子句如果存在,它必须放在所有 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()
8.3.3 try-…-finally
try
语句还有一个可选子句finally
,用于定义在所有情况下都必须要执行的清理操作。例如:
try: raise KeyboardInterrupt finally: print('Goodbye, world!')
不论 try
语句是否触发异常,都会执行 finally
子句。在实际应用程序中,finally
子句对于释放外部资源(例如文件或者网络连接)非常有用。
with 语句是try-finally的一种简化写法,相当于在后面隐藏了一个finally来清理资源:
with open("myfile.txt") as f: for line in f: print(line, end="")
try-finally 特殊情形:
以下内容介绍了几种比较复杂的触发异常情景:
- 如果执行
try
子句期间触发了某个异常,则某个except
子句应处理该异常。如果该异常没有except
子句处理,在finally
子句执行后会被重新触发。 except
或else
子句执行期间也会触发异常。 同样,该异常会在finally
子句执行之后被重新触发。- 如果
finally
子句中包含break
、continue
或return
等语句,异常将不会被重新引发。 - 如果执行
try
语句时遇到break
,、continue
或return
语句,则finally
子句在执行break
、continue
或return
语句之前执行。 - 如果
finally
子句中包含return
语句,则返回值来自finally
子句的某个return
语句的返回值,而不是来自try
子句的return
语句的返回值。
8.4 抛出异常
8.4.1 raise 异常
raise
语句可以抛出指定的异常:
raise 异常
raise NameError('HiThere')
在捕获异常后如果不想处理,可以用单个raise
重新抛出异常:
try: raise NameError('HiThere') except NameError: print('An exception flew by!') raise
8.4.2 异常链 raise from
raise
支持可选的 from
子句,用于启用链式异常。
如:raise RuntimeError from exc
转换异常时,这种方式很有用。例如:
def func(): raise ConnectionError try: func() except ConnectionError as exc: raise RuntimeError('Failed to open database') from exc
异常链会在 except
或 finally
子句内部引发异常时自动生成。 这可以通过使用 from None
这样的写法来禁用:
try: open('database.sqlite') except OSError: raise RuntimeError from None
8.3 用户自定义异常
用户可以通过自定义继承Exception
的类来实现自己的异常。大多数异常命名都以 “Error” 结尾,类似标准异常的命名。(第9章类将介绍如何定义类)
class MyError(Exception): def __init__(self, value): self.value = value def __str__(self): return repr(self.value) try: raise MyError(42) except MyError as e: print(e)