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
子句执行之后被重新触发。如果执行
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)