如何在Python中捕获异常

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异常,也分享了一些更微妙的功能。

感谢您花时间阅读文章
关注公众号不迷路:
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值