异常处理
约 2545 字大约 8 分钟
2025-04-04
概述
如果我们在 Python 终端中执行 1 / 0
,会有这样的结果:
>>> 1 / 0
Traceback (most recent call last):
File "<python-input-0>", line 1, in <module>
1 / 0
~~^~~
ZeroDivisionError: division by zero
我们没有得到任何结果(事实上也不该有什么结果),而是出现了一个错误,告诉我们 0 不能做除数。这会导致什么问题呢?看下面这段代码:
print('我在异常出现前执行')
1 / 0
print('我在异常出现后执行')
执行后的结果如下:
我在异常出现前执行
Traceback (most recent call last):
File "/Users/xxx/Documents/t.py", line 2, in <module>
1 / 0
ZeroDivisionError: division by zero
可以看到,异常出现前的代码正常执行,而异常出现后的代码却没有被执行
对于稍微大型的项目来说,程序出现异常是不可避免的,而程序一旦出现异常就会直接终止,这对于线上服务来说往往是不可接受的。所以我们需要具备能够处理异常的能力,当异常发生时,我们往往需要做些补救措施,而不是让程序直接崩溃
除了避免程序崩溃之外,因为异常可以让程序直接终止运行,有时候我们可能还会巧妙地运用这个特点,人为抛出一些异常,让程序实现一些高效的逻辑跳转(这也是为什么我把异常处理放在流程控制这个模块中)
基本用法
Python 内置了很多异常,比如刚刚出发的不能除零异常 ZeroDivisionError
,还有我们之前学习过程中可能会遇到过的 KeyError
、ValueError
,等等。Python 提供了一种特定的语法结构用来进行异常处理():
try:
被检测的代码块
except 异常类型 as e: # e 是变量名,可以随便叫,甚至 as e 如果后面不用的话可以不写,直接写成 except 异常类型
异常处理
后续代码
这个语法结构的含义是,尝试运行被检测的代码块,如果没有异常,则直接走出 try except
结构,执行后续代码。如果有异常,且异常类型与 except 后的类型相符,则会先执行异常处理逻辑,然后执行后续代码。如果抛出的异常不是 except
后的类型,则仍旧会抛出异常,导致程序终止。
运行如下代码,尝试输入数字 1
、数字 100
、字母 a
,看运行结果
d = {
1: '一',
2: '二',
3: '三',
}
while True:
try:
key = int(input('输入 1 - 3 的整数:>>>'))
print('没出错')
except ValueError as e:
print('需要输入整数')
程序运行结果是这样的:
这说明两个问题:
- 如果异常被捕获,程序将继续运行
- 如果异常没有被捕获(比如我们捕获的是
ValueError
,但是输入100
后会触发KeyError
),则异常仍旧会抛出
如果我们想要捕获更多类型的异常,可以这么写:
try:
被检测的代码块
except 异常类型 A as e:
处理异常 A 的逻辑
except (异常类型 B, 异常类型 C) as e:
处理 B 或 C 异常的逻辑
后续代码
也就是说,我们可以额外增加一条 except
语句,也可以将处理逻辑相同的异常放到元组中统一捕获
运行如下代码,尝试输入数字 1
、数字 2
、数字 100
、字母 a
,看运行结果:
d = {
1: '一',
2: '二',
3: '三',
}
while True:
try:
key = int(input('输入 1 - 3 的整数:>>>'))
key / (key - 1) # 这段代码会在输入 1 的时候抛出 ZeroDivisionError 异常
value = d[key]
print(f'你输入了数字{value}')
except ValueError:
print('需要输入整数')
except (KeyError, ZeroDivisionError):
print('你输入的整数有问题哦!')
万能异常
如果我们知道可能会有哪些类型的异常,最好的方式还是写清楚异常类型。但是有的时候,我们可能会有一些意想不到的异常,这时,就需要用万能异常 Exception
统一管理,这样我们就可以捕获任意异常,写法如下:
try:
被检测的代码块
except Exception as e:
异常处理逻辑
其实在 Python 的异常处理中,except Exception
和仅使用 except
都可以用来捕获任意异常,那么这两个写法之间有什么区别吗?答案是有的,裸 except
捕获的异常更加“万能”,即能捕获更加底层的系统级别的异常(如用户按 Ctrl+C
触发的 KeyboardInterrupt
正常终止程序),而 except Exception
仅能捕获继承自 Exception
的异常
实操一下,终端执行下面这行代码:
while True:
try:
print(int(input('输入一个数字:>>>')))
except Exception as e:
print('捕获到 Exception')
except:
print('捕获到 BaseException')
当我们输入普通数字时,正常打印没什么问题。当我们输入一个字母,会出发 ValueError
,它继承自 Exception
,所以会被 except Exception
捕获。而让我们尝试使用 Ctrl+C
退出时,因为是系统异常,不属于 Exception
,所以被最后的裸 except
捕获。可以看到,裸 except
捕获异常的级别很高,使用 Ctrl+C
甚至都无法退出程序了。大多数时候我们只需要捕获 Exception
级别的异常就足够了
手动触发异常
蝮蛇螫手,壮士解腕,有的时候我们发现程序运行可能有点问题,如果继续运行可能会造成更严重的错误,这时我们可能会主动抛出一个异常,强行终止后续的处理逻辑(当然外层可能还会包裹一层 try
来监听我们抛出的异常,避免程序崩溃)。另外,有时我们会借助异常会终止后续代码运行这一特性,实现一些特殊的逻辑跳转功能(比如像跳出多层循环或函数嵌套,一个 break
或 return
只能退出一层,手动抛出异常会直接从嵌套中跳转出来。但一定要慎重使用,因为这个用法和 C 中的 goto
语句很像,代码可读性很低)
Python 使用 raise
语句手动抛出异常,语法如下:
try:
raise TypeError('类型错误')
except TypeError as e:
print(e)
跳出多层循环的例子:
try:
for i in range(1, 10):
for j in range(1, 10):
for k in range(1, 10):
if i == j == k == 5:
raise Exception
except Exception:
print(f'现在 i、j、j 的值为:{i}、{j}、{k}')
断言
断言也是手动触发异常的一种方式,关键字是 assert
。Python 断言的逻辑是 assert
后面的语句执行结果为真,无任何异常;如果 assert
后面的语句执行结果为假,会抛出 AssertionError
异常。断言在测试代码中应用十分广泛
尝试执行着两行代码:
assert 1 == 1 # 无异常
assert 1 == 2 # AssertionError
自定义异常
系统自带的异常有时不能满足我们的需求,这时就需要自定义异常。Python 自定义异常很简单,只需要写一个继承 Exception 类的子类即可:
class MyException(Exception):
pass
try:
raise MyException('我的异常')
except MyException as e:
print(e)
这里有一点主要注意的是,有些文章写的自定义异常继承的是 BaseException
类,这是不对的。从 Python 官方文档 中可以看到,只有系统异常才能继承 BaseException
类,用户异常都应该继承 Exception
类。正如我们前面讨论的那样,继承BaseException
类的异常是没法被 except Exception
捕获的,这一点需要注意一下
try else
在异常处理的基本用法中:
前置逻辑
try:
主要逻辑
except Exception:
异常处理
后序逻辑
异常处理
代码只有在 主要逻辑
代码发生异常的时候才会执行,而 后序逻辑
代码无论是否发生异常都会处理,如果我们想要实现 主要逻辑
代码有异常的时候不执行,没有异常的时候才会执行,该如何处理呢?答案很简单,我们可以把这段代码在 except
后面再加一个 else
语句实现:
前置逻辑
try:
主要逻辑
except Exception:
异常处理
else:
主要逻辑无异常时执行
后续逻辑
可以尝试输入数字和字母,看看如下代码的输出结果:
while True:
try:
print(int(input('输入一个数字:>>>')))
except ValueError:
print('出错啦')
else:
print('没出错')
finally
finally
是无论如何一定会执行的代码,无论是否发生异常,甚至 continue
、break
、return
前都要执行。有时候为了确保系统资源正确释放,我们可能会把这些逻辑放到 finally
中
尝试执行这些代码:
# 异常
for i in [1, 2, 3]:
try:
assert i != 2
finally:
print(f'finally 怎么着都会被执行哈 i = {i}')
# 异常被捕获
for i in [1, 2, 3]:
try:
assert i != 2
except AssertionError:
print(f'异常被捕获 i = {i}')
finally:
print(f'finally 怎么着都会被执行哈 i = {i}')
# continue
for i in [1, 2, 3]:
try:
if i == 2:
continue
finally:
print(f'finally 怎么着都会被执行哈 i = {i}')
# break
for i in [1, 2, 3]:
try:
if i == 2:
break
finally:
print(f'finally 怎么着都会被执行哈 i = {i}')
# return
def f():
try:
return 666
finally:
print('finally 怎么着都会被执行哈')
f()
traceback
当我们使用 try
把异常捕获之后,异常的内容也不会被打印的。但是有的时候,我们想要的效果是,异常被捕获以后,程序不终止,但是终端或者日志中还是会出现异常信息,这时我们可以使用 traceback 模块中的 format_exc
方法:
from traceback import format_exc
print('前面的逻辑')
try:
1 / 0
except ZeroDivisionError:
print(format_exc())
print('后面的逻辑')
执行过后不难发现,前面的逻辑和后面的逻辑都会被执行,这意味着程序没有被终止。同时,异常信息也被打印出来,可以方便我们后续排查:
版权所有
版权归属:Shuo Liu