>>> from env_helper import info; info()
页面更新时间: 2022-08-29 22:11:10
运行环境:
    Linux发行版本: Debian GNU/Linux 11 (bullseye)
    操作系统内核: Linux-5.10.0-17-amd64-x86_64-with-glibc2.31
    Python版本: 3.9.2

7.2. 捕获异常

异常比较有趣的地方是可对其进行处理,通常称之为捕获异常。为此,可使用 try/except语句。假设你创建了一个程序,让用户输入两个数,再将它们相除,如下所示:

7.2.1. Python中的异常处理

>>> x = int(input('Enter the first number: '))
>>> y = int(input('Enter the second number: '))
>>> print(x / y)
Enter the first number:  3
Enter the second number:  0
---------------------------------------------------------------------------

ZeroDivisionError                         Traceback (most recent call last)

<ipython-input-1-bba3ed2748f0> in <module>
      1 x = int(input('Enter the first number: '))
      2 y = int(input('Enter the second number: '))
----> 3 print(x / y)


ZeroDivisionError: division by zero

这个程序运行正常,直到用户输入的第二个数为零。

为捕获这种异常并对错误进行处理,可像下面这样重写这个程序:

>>> try:
>>>     x = int(input('Enter the first number: '))
>>>     y = int(input('Enter the second number: '))
>>>     print(x / y)
>>> except ZeroDivisionError:
>>>     print("The second number can't be zero!")
Enter the first number:  3
Enter the second number:  0
The second number can't be zero!

使用一条 if 语句来检查y的值好像简单些,就本例而言,这可能也是更佳的解决方案。 正确的实践是:我们使用 if 语句来检查已知的例外情况;而使用异常在捕捉未预料到的“异常”的情况。

注意 异常从函数向外传播到调用函数的地方。 如果在这里也没有被捕获,异常将向程序的最顶层传播。 这意味着你可使用 try/except 来捕获他人所编写函数引发的异常。

7.2.2. 不用提供参数

捕获异常后,如果要重新引发它(即继续向上传播),可调用raise且不提供任何参数。

为说明这很有用,来看一个能够“抑制”异常ZeroDivisionError的计算器类。 如果启用了这 种功能,计算器将打印一条错误消息,而不让异常继续传播。 在与用户交互的会话中使用这个计算器时,抑制异常很有用; 但在程序内部使用时,引发异常是更佳的选择(此时应关闭“抑制” 功能)。

下面是这样一个类的代码:

>>> class MuffledCalculator:
>>>     muffled = False
>>>
>>>     def calc(self, expr):
>>>         try:
>>>             return eval(expr)
>>>         except ZeroDivisionError:
>>>             if self.muffled:
>>>                 print('Division by zero is illegal')
>>>             else:
>>>                 raise

注意

发生除零行为时,如果启用了“抑制”功能,方法calc将(隐式地)返回None。换而言 之,如果启用了“抑制”功能,就不应依赖返回值。

下面的示例演示了这个类的用法:

>>> calculator = MuffledCalculator()
>>> calculator.calc('10 / 0')
---------------------------------------------------------------------------

ZeroDivisionError                         Traceback (most recent call last)

<ipython-input-8-5cf3823d3fd7> in <module>
      1 calculator = MuffledCalculator()
----> 2 calculator.calc('10 / 0')


<ipython-input-6-a3a8e2857bb1> in calc(self, expr)
      4     def calc(self, expr):
      5         try:
----> 6             return eval(expr)
      7         except ZeroDivisionError:
      8             if self.muffled:


<string> in <module>


ZeroDivisionError: division by zero
>>> calculator.calc('10 / 0') # 关闭了抑制功能
Traceback (most recent call last): File "<stdin>", line 1, in ?
    File "MuffledCalculator.py", line 6, in calc
        return eval(expr)
    File "<string>", line 0, in ?
ZeroDivisionError: integer division or modulo by zero
>>> calculator.muffled = True
>>> calculator.calc('10 / 0')
Division by zero is illegal

如你所见,关闭抑制功能时,捕获了异常ZeroDivisionError,但继续向上传播它。 如果无法处理异常,在except子句中使用不带参数的raise通常是不错的选择,但有时你可 能想引发别的异常。在这种情况下,导致进入except子句的异常将被作为异常上下文存储起来, 并出现在最终的错误消息中,如下所示:

>>> try:
...
1/0
... except ZeroDivisionError:
...
raise ValueError
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero

在处理上述异常时,引发了另一个异常:

Traceback (most recent call last):
File "<stdin>", line 4, in <module>
ValueError

你可使用raise … from …语句来提供自己的异常上下文,也可使用None来禁用上下文。

>>> try:
...
1/0
... except ZeroDivisionError:
...
raise ValueError from None
...
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
ValueError

7.2.3. 多个 except 子句

如果你运行前一节的程序,并在提示时输入一个非数字值,将引发另一种异常。

Enter the first number: 10
Enter the second number: "Hello, world!"
Traceback (most recent call last):
File "exceptions.py", line 4, in ?
print(x / y)
TypeError: unsupported operand type(s) for /: 'int' and 'str'

由于该程序中的except子句只捕获ZeroDivisionError异常,这种异常将成为漏网之鱼,导致程序终止。为同时捕获这种异常,可在try/except语句中再添加一个except子句。

try:

    x = int(input('Enter the first number: '))
    y = int(input('Enter the second number: ')) print(x / y)

except ZeroDivisionError:
    print("The second number can't be zero!")
except TypeError:
    print("That wasn't a number, was it?")

现在使用if语句来处理将更加困难。如何检查一个值能否用于除法运算呢?方法有很多,但最佳的方法无疑是尝试将两个值相除,看看是否可行。

7.2.4. 一箭双雕

如果要使用一个except子句捕获多种异常,可在一个元组中指定这些异常,如下所示:

try:
    x = int(input('Enter the first number: '))
    y = int(input('Enter the second number: '))
    print(x / y)
except (ZeroDivisionError, TypeError, NameError):
    print('Your numbers were bogus ...')

在上述代码中,如果用户输入字符串、其他非数字值或输入的第二个数为零,都将打印同样 的错误消息。当然,仅仅打印错误消息帮助不大。另一种解决方案是不断地要求用户输入数字,直到能够执行除法运算为止。 在except子句中,异常两边的圆括号很重要。一种常见的错误是省略这些括号,这可能导致你不想要的结果。

7.2.5. 捕获对象

要在except子句中访问异常对象本身,可使用两个而不是一个参数。(请注意,即便是在你 捕获多个异常时,也只向except提供了一个参数——一个元组。)需要让程序继续运行并记录错 误(可能只是向用户显示)时,这很有用。下面的示例程序打印发生的异常并继续运行:

try:
    x = int(input('Enter the first number: '))
    y = int(input('Enter the second number: '))
    print(x / y)
except (ZeroDivisionError, TypeError) as e:
    print(e)

在这个小程序中,except子句也捕获两种异常,但由于你同时显式地捕获了对象本身,因此可将其打印出来,让用户知道发生了什么情况。

7.2.6. 捕获所有异常

即使程序处理了好几种异常,还是可能有一些漏网之鱼。例如,对于前面执行除法运算的程 序,如果用户在提示时不输入任何内容就按回车键,将出现一条错误消息,还有一些相关问题出 在什么地方的信息(栈跟踪),如下所示:

Traceback (most recent call last):
...
ValueError: invalid literal for int() with base 10: ''

这种异常未被try/except语句捕获,这理所当然,因为你没有预测到这种问题,也没有采取 相应的措施。在这些情况下,与其使用并非要捕获这些异常的try/except语句将它们隐藏起来, 还不如让程序马上崩溃,因为这样你就知道什么地方出了问题。

然而,如果你就是要使用一段代码捕获所有的异常,只需在except语句中不指定任何异常类 即可。

try:
    x = int(input('Enter the first number: '))
    y = int(input('Enter the second number: '))
    print(x / y)
except:
    print('Something wrong happened ...')

现在,用户想怎么做都可以。

Enter the first number: "This" is *completely* illegal 123 Something wrong happened ...

像这样捕获所有的异常很危险,因为这不仅会隐藏你有心理准备的错误,还会隐藏你没有考 虑过的错误。这还将捕获用户使用Ctrl + C终止执行的企图、调用函数sys.exit来终止执行的企图 等。在大多数情况下,更好的选择是使用except Exception as e并对异常对象进行检查。这样做 将让不是从 Exception 派生而来的为数不多的异常成为漏网之鱼,其中包括 SystemExit 和 KeyboardInterrupt,因为它们是从BaseException(Exception的超类)派生而来的。

7.2.7. 没有异常时

在有些情况下,在没有出现异常时执行一个代码块很有用。为此,可像条件语句和循环一样, 给try/except语句添加一个else子句。

>>> try:
>>>     print('A simple task')
>>> except:
>>>     print('What? Something went wrong?')
>>> else:
>>>     print('Ah ... It went as planned.')
A simple task
Ah ... It went as planned.

通过使用else子句,可实现循环。

while True:
    try:
        x = int(input('Enter the first number: '))
        y = int(input('Enter the second number: '))
        value = x / y
        print('x / y is', value)
    except:
        print('Invalid input. Please try again.')
    else:
        break

在这里,仅当没有引发异常时,才会跳出循环。换而言之,只要出现错误,程序就会要求用户提供新的输入。下面是这些代码的运行情况:

Enter the first number: 1
Enter the second number: 0
Invalid input. Please try again.
Enter the first number: 'foo'
Enter the second number: 'bar'
Invalid input. Please try again.
Enter the first number: baz
Invalid input. Please try again.
Enter the first number: 10
Enter the second number: 2
x / y is 5

一种更佳的替代方案是使用空的except子句来捕获所有属于类Exception(或其子类)的异常。你不能完全确定这将捕获所有的异常,因为try/except语句中的代码可能使用旧式的字符串异常或引发并非从Exception派生而来的异常。

while True:
    try:
        x = int(input('Enter the first number: '))
        y = int(input('Enter the second number: '))
        value = x / y
        print('x / y is', value)
    except Exception as e:
        print('Invalid input:', e)
        print('Please try again')
    else:
        break

下面是这个程序的运行情况:

Enter the first number: 1
Enter the second number: 0
Invalid input: integer division or modulo by zero
Please try again
Enter the first number: 'x' Enter the second number: 'y'
Invalid input: unsupported operand type(s) for /: 'str' and 'str'
Please try again
Enter the first number: quuux
Invalid input: name 'quuux' is not defined
Please try again
Enter the first number: 10
Enter the second number: 2
x / y is 5

7.2.8. finally 子句

最后,还有 finally 子句,可用于在发生异常时执行清理工作。这个子句是与 try 子句配套的。

x = None
try:
    x = 1 / 0
finally:
    print('Cleaning up ...')
    del x

在上述示例中,不管try子句中发生什么异常,都将执行finally子句。为何在try子句之前 初始化x呢?因为如果不这样做,ZeroDivisionError将导致根本没有机会给它赋值,进而导致在 finally子句中对其执行del时引发未捕获的异常。

如果运行这个程序,它将在执行清理工作后崩溃。

Cleaning up ...
Traceback (most recent call last):
    File "C:\python\div.py", line 4, in ?
        x = 1 / 0
ZeroDivisionError: integer division or modulo by zero

也可在一条语句中同时包含try、except、finally和else(或其中的3个)。

>>> try:
>>>     1 / 0
>>> except NameError:
>>>     print("Unknown variable")
>>>
>>> else:
>>>     print("That went well!")
>>>
>>> finally:
>>>     print("Cleaning up.")
Cleaning up.
---------------------------------------------------------------------------

ZeroDivisionError                         Traceback (most recent call last)

<ipython-input-2-85b1275690ae> in <module>()
      1 try:
----> 2     1 / 0
      3
      4 except NameError:
      5     print("Unknown variable")


ZeroDivisionError: division by zero