1. 写在前面
本文主要介绍 Python 捕获异常的各种技术。首先,回顾 Python 的异常处理机制,然后深入研究并学习如何识别捕获的异常内容,以及忽略异常。
公众号: 滑翔的纸飞机
2. Python 异常处理机制
Python 代码在运行的过程中,偶尔将出现意料之内或之外的错误从而引发异常。例如,如果尝试读取不存在的文件,就会发生这种情况。因为意料到可能会发生此类异常,所以应该编写代码来处理异常。相反,当你的代码执行不合逻辑操作时,也会发生错误。该类错误应该被修复,而不是处理。
当你的 Python 程序遇到错误并引发异常时,代码很可能会崩溃,但在崩溃停止之前会在控制台输出错误,说明问题所在:
例如:
>>> 12/'3'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for /: 'int' and 'str'
该示例,试图用一个数字除以一个字符串。Python 不支持,因此会引发 TypeError 异常。然后它会显示一个错误堆栈,提醒除法运算符对字符串不起作用。
为了在发生错误时采取措施,需要编写代码来捕获和处理异常,从而实现异常处理。这样做总比代码崩溃、影响用户要友好的多。要处理异常,Python中需要使用 try 语句。监控代码是否出现异常,并在出现异常时采取相应措施。
使用 try ... except ... else ... finally块
:
try:正常情况下,程序计划执行的语句。
except:程序异常是执行的语句。
else:程序无异常即try段代码正常执行后会执行该语句。
finally:不管有没有异常,都会执行的语句。
try:
<语句> #运行别的代码
except <名字>:
<语句> #如果在try部份引发了'name'异常
except <名字>,<数据>:
<语句> #如果引发了'name'异常,获得附加的数据
else:
<语句> #如果没有异常发生
finally:
<语句> #无论异常是否出发,都将执行
- try 代码块包含你希望监控异常的代码。任何在其中引发的异常都将得到处理。
- 接着是一个或多个 except 块。可以在这些块中定义异常发生时运行的代码。在代码中,任何异常都会触发相关 except。注意,如果多个 except,程序将只运行第一个符合的 except ,而忽略其余 except 。
要了解如何工作,需要编写一个代码块。其中包括两个except代码块,分别用于处理 ValueError 和 ZeroDivisionError 异常,以便在异常发生时进行处理:
"""
@Time:2023/9/24 16:31
@Author:'jpzhang.ht@gmail.com'
@Describe:
"""
try:
first = float(input("What is your first number? "))
second = float(input("What is your second number? "))
print(f"{first} divided by {second} is {first / second}")
except ValueError:
print("You must enter a number")
except ZeroDivisionError:
print("You can't divide by zero")
这段代码示例,要求用户输入两个数字(除数、被除数),并打印输出结果,如果用户输入不是数字,或者 float() 函数尝试将输入转换为浮点数时,如果无法转换,都将会引发ValueError。如果输入的第二个数字是 0,则会出现 ZeroDivisionError。这里 print() 函数尝试除以 0 时,会出现零点整除错误。
What is your first number? 2
What is your second number? 3
2.0 divided by 3.0 is 0.6666666666666666
What is your first number? 2
What is your second number? '5'
You must enter a number
What is your first number? 2
What is your second number? 0
You can't divide by zero
异常处理完成后,程序会继续执行 try 语句以外的代码。在本例中,没有其他任何代码,因此程序直接结束。
【注意】:示例代码只能捕获 ZeroDivisionError 或 ValueError 异常。如果出现其他异常,则会像以前一样崩溃。你可以通过创建一个 except Exception 子句来捕获所有其他异常。然而,这种做法并不可取,因为你可能会捕获到你没有预料到的异常。最好明确地捕获异常,并自定义对异常的处理。
通过简单示例,了解Python 异常处理机制,接下去步入正题。了解更多处理异常方式;
3. Python 捕获异常常用技巧
3.1 如何捕获几种可能的 Python 异常,并执行共同的处理?
如果需要对捕获的不同异常执行不同的处理操作,那么在单独的异常子句(except)中捕获单个异常是个不错的选择。如果你发现在处理不同异常时执行了相同的操作,那么你可以在单个异常子句中处理多个异常,从而编写出更简单、更易读的代码。为此,可以在 except 语句中以元组的形式指定异常。
假设,现在需要在之前的代码中,能够在一行中同时处理两种异常(ValueError、ZeroDivisionError),重写代码如下:
try:
first = float(input("What is your first number? "))
second = float(input("What is your second number? "))
print(f"{first} divided by {second} is {first / second}")
except (ValueError, ZeroDivisionError) as error:
print("There was an error")
现在,无论捕获 ValueError 或 ZeroDivisionError 异常,都将使用相同的 except 子句来处理。当然,也可以为其他异常添加额外的 except 子句,添加方式一样。
进一步思考:虽然 except 以相同的方式安全地处理了这两个异常,但如果你想知道到底是哪个异常被触发了。显然当前的处理方式并不能做到,接下来将学习如何做到这一点。
3.2 如何识别哪个 Python 异常被捕获?
如果你比较熟悉面向对象编程概念,那么你应该知道类是一种模板,它定义了实例化对象的内容。当你的代码引发 Python 异常时,它实际上是从定义异常的类中实例化了一个对象。例如,当代码引发一个 ValueError 异常时,其实是实例化了一个 ValueError 类的实例。
虽然异常处理对面向对象编程知识要求不高,但需要了解,之所以存在不同的异常对象,是因为它们是从不同的类中实例化出来的。
现在如果我们要识别之前代码中捕获的各个异常,可以通过如下实现:
try:
first = float(input("What is your first number? "))
second = float(input("What is your second number? "))
print(f"{first} divided by {second} is {first / second}")
except (ValueError, ZeroDivisionError) as error:
print(f"A {type(error).__name__} has occurred.")
输出:
What is your first number? 2
What is your second number? 0
A ZeroDivisionError has occurred.
What is your first number? 2
What is your second number? '2'
A ValueError has occurred.
这里对异常处理做了一些改进,不仅可以捕获 ValueError 和 ZeroDivisionError 异常,同时也将捕获的异常对象赋值给一个名为 error 的变量,这样可以对其进行进一步分析;
type()
: 查看异常对象类型信息;
.__name__
: 获取类名;
在看一个稍复杂点的例子:
from operator import mul, truediv
def calculate(operator, operand1, operand2):
return operator(operand1, operand2)
try:
first = float(input("What is your first number? "))
second = float(input("What is your second number? "))
operation = input("Enter either * or /: ")
if operation == "*":
answer = calculate(mul, first, second)
elif operation == "/":
answer = calculate(truediv, first, second)
else:
raise RuntimeError(f"'{operation}' is an unsupported operation")
except (RuntimeError, ValueError, ZeroDivisionError) as error:
print(f"A {type(error).__name__} has occurred")
match error:
case RuntimeError():
print(f"You have entered an invalid symbol: {error}")
case ValueError():
print(f"You have not entered a number: {error}")
case ZeroDivisionError():
print(f"You can't divide by zero: {error}")
else:
print(f"{first} {operation} {second} = {answer}")
**代码说明:**通过 operator 模块包含的 mul() 和 truediv() 函数来执行乘/除运算。程序根据用户输入将函数和数字传递给 calculate() 函数,calculate() 函数调用传递给它的运算符模块函数,执行计算。现在只有输入两个数字以及 ‘/’ 或 ‘*’ 进行运算时,该函数才会起作用。
【提示】:calculate() 函数可以直接使用 '* '或 ‘/’ 操作符,不过使用 mul()/truediv() 函数可以简化代码,提高可扩展性。
如果用户输入了无效的运算符,代码将显式抛出 RuntimeError 异常。
except 块和之前一样,额外增加了 RuntimeError 异常的捕捉,在 except 块中,根据异常类型匹配打印不同的消息。
try 块没有异常,将执行 else 代码块,打印执行结果。
3.3 如何使用超类捕获 Python 多种异常?
不同异常是从不同的类中实例化出来的。这些类都属于 Python 异常类。所有 Python 异常都继承自一个名为 BaseException 的类,其中一个子类就是 Exception 类。它是本文中要学习的所有异常的超类。
Python 包含六十多种不同的异常。下图只说明了其中的几种,但它包含了本教程中要介绍的所有异常子类。事实上,这些都是你可能会遇到的一些常见异常:
如上图,Exception 是所有其他异常的超类。Exception 的子类继承了 Exception 包含的所有内容。继承主要是为了创建异常层次结构。例如:ArithmeticError 是 Exception 的子类。从代码来看,它们之间的差异可以忽略不计。再看 OSError 类的两个子类(PermissionError、FileNotFoundError)。由于 OSError 继承自 Exception,因此也是 Exception 的子类。
我们可以利用子类是其超类的变体这一事实来捕获不同的异常。如以下代码:
from os import strerror
try:
with open("datafile.txt", mode="rt") as f:
print(f.readlines())
except OSError as error:
print(strerror(error.errno))
os 就是“operating system”的缩写,顾名思义, os 模块提供的就是各种 Python 程序与操作系统进行交互的接口。os.strerror() 方法用于获取与错误代码对应的错误消息。
如果名为 datafile.txt 文件存在,代码将打印该文件的内容。如果 datafile.txt 不存在,代码就会引发 FileNotFoundError。虽然只包含了一个 except 子句,看起来只能捕获 OSError,但处理程序也可以处理 FileNotFoundError,因为它实际上是一个 OSError 的子类。
如果要确定捕获的是 OSError 的哪个子类,可以使用 type(error).__name__
来打印它的类名。然而,这对大多数用户来说也毫无意义。相反,你可以通过 .errno 属性来识别底层错误。这是操作系统生成的一个数字,提供了引发 OSError 异常的相关信息。数字本身没有意义,但其相关的错误信息会告诉你更多有关问题的信息。
例如这里,异常处理程序使用变量 error,该变量引用了 OSError 异常的子类。要查看相关的错误信息,可以将错误代码传入 os.strerror() 函数。当你打印函数的输出时,你就会知道到底哪里出错了:
本示例输出:
No such file or directory
也可以尝试以下场景,用于验证是否可以捕获 PermissionError 异常:
创建文件 datafile.txt,但确保没有访问它的权限。然后再次尝试重新运行代码,并验证代码是否识别并处理异常。
3.4 如何忽略多个 Python 异常?
【重点】contextlib.suppress()
通常,当代码遇到异常时,会想要处理它。但有时,可能需要忽略异常,以使代码正常工作。例如,从可能被其他用户锁定的文件中读取数据。
在 Python 中忽略异常的传统方法是捕获异常但不做任何处理:
try:
with open("file.txt", mode="rt") as f:
print(f.readlines())
except (FileNotFoundError, PermissionError):
pass
如上示例,捕获 FileNotFoundError、PermissionError 异常后不做处理,没有任何输出且程序正常运行。但代码可读性差,程序虽然捕获异常但不做处理。
这里介绍另一种更简洁的处理方式:
要编写明确忽略异常的代码,Python 提供了一个上下文管理器。通过 contextlib 模块来实现。
from contextlib import suppress
with suppress(FileNotFoundError, PermissionError):
with open("file.txt", mode="rt") as f:
print(f.readlines())
通过创建上下文管理器来忽略异常。
3.5 使用异常组捕获多个 Python 异常
当使用时 try… except 时,它实际只能捕获 try 块中出现的第一个异常。如果触发多个异常,程序将在处理完第一个异常后结束。其余的异常永远不会被触发。可以通过以下代码进行验证。
exceptions = [ZeroDivisionError(), FileNotFoundError(), NameError()]
num_zd_errors = num_fnf_errors = num_name_errors = 0
try:
for e in exceptions:
raise e
except ZeroDivisionError:
num_zd_errors += 1
except FileNotFoundError:
num_fnf_errors += 1
except NameError:
num_name_errors += 1
finally:
print(f"ZeroDivisionError was raised {num_zd_errors} times.")
print(f"FileNotFoundError was raised {num_fnf_errors} times.")
print(f"NameError was raised {num_name_errors} times.")
定义包含三个异常对象的列表。异常统计计数置0。循环触发异常(当然这里并不严谨),可以看到输出:
ZeroDivisionError was raised 1 times.
FileNotFoundError was raised 0 times.
NameError was raised 0 times.
仅有一个异常被捕获处理;
但有时可能需要处理所有发生的异常。例如,在并发编程(程序同时执行多个任务)中就需要这样做。在有并发任务运行的情况下,每个任务都会引发自己的异常。从 Python 3.11 开始,支持 ExceptionGroup 对象和一个特殊子句(exceptions.except*
),允许处理所有异常。
调整先前的代码,演示如何处理多个异常。使用特殊语法:except*
exceptions = [ZeroDivisionError(), FileNotFoundError(), NameError()]
num_zd_errors = num_fnf_errors = num_name_errors = 0
try:
raise ExceptionGroup("Errors Occurred", exceptions)
except* ZeroDivisionError:
num_zd_errors += 1
except* FileNotFoundError:
num_fnf_errors += 1
except* NameError:
num_name_errors += 1
finally:
print(f"ZeroDivisionError was raised {num_zd_errors} times.")
print(f"FileNotFoundError was raised {num_fnf_errors} times.")
print(f"NameError was raised {num_name_errors} times.")
try 块触发一个异常对象,该对象实例化时,将包含列表内容,它会将所有异常传递给处理程序。同时为了确保处理程序可以处理所有异常,这里 except <异常>
语法替代为 except* <异常>
,当触发异常时,except* 块将处理任何异常。未匹配的异常通过每个 except* 向下传递。
ZeroDivisionError was raised 1 times.
FileNotFoundError was raised 1 times.
NameError was raised 1 times.
从输出中可以看出,程序成功地处理了所有异常,并正确增加了所有三个变量值。
4. 最后
本文除了介绍Python异常,也分享了一些更微妙的功能。