异 常
8.3 捕获异常
前面说过,异常比较有趣的地方是可对其进行处理,通常称之为捕获异常。为此,可使用
try/except语句。假设你创建了一个程序,让用户输入两个数,再将它们相除,如下所示:
x = int(input('Enter the first number: ')) y = int(input('Enter the second number: ')) print(x / y)
这个程序运行正常,直到用户输入的第二个数为零。
Enter the first number: 10 Enter the second number: 0 Traceback (most recent call last):
File "exceptions.py", line 3, in ? print(x / y)
ZeroDivisionError: integer division or modulo 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!")
使用一条if语句来检查y的值好像简单些,就本例而言,这可能也是更佳的解决方案。然而,
如果这个程序执行的除法运算更多,则每个除法运算都需要一条if语句,而使用try/except的话 只需要一个错误处理程序。
注意 异常从函数向外传播到调用函数的地方。如果在这里也没有被捕获,异常将向程序的最 顶层传播。这意味着你可使用try/except来捕获他人所编写函数引发的异常。有关这方面 的详细信息,请参阅8.4节。
8.3.1 不用提供参数
捕获异常后,如果要重新引发它(即继续向上传播),可调用raise且不提供任何参数(也可 显式地提供捕获到的异常,参见8.3.4节)。
为说明这很有用,来看一个能够“抑制”异常ZeroDivisionError的计算器类。如果启用了这 种功能,计算器将打印一条错误消息,而不让异常继续传播。在与用户交互的会话中使用这个计 算器时,抑制异常很有用;但在程序内部使用时,引发异常是更佳的选择(此时应关闭“抑制”
功能)。下面是这样一个类的代码:
1
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 / 2') 5.0
>>> 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
8.3.2 多个 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语句来处理将更加困难。如何检查一个值能否用于除法运算呢?方法有很多,但 最佳的方法无疑是尝试将两个值相除,看看是否可行。
另外,注意到异常处理并不会导致代码混乱,而添加大量的if语句来检查各种可能的错误状 态将导致代码的可读性极差。
8.3.3 一箭双雕
如果要使用一个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 ...')
在上述代码中,如果用户输入字符串、其他非数字值或输入的第二个数为零,都将打印同样 的错误消息。当然,仅仅打印错误消息帮助不大。另一种解决方案是不断地要求用户输入数字,
1
except (ZeroDivisionError, TypeError) as e:
print(e)
Traceback (most recent call last):
...
ValueError: invalid literal for int() with base 10: ''
这种异常未被try/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的超类)派生而来的。
8.3.6 万事大吉时
在有些情况下,在没有出现异常时执行一个代码块很有用。为此,可像条件语句和循环一样,
给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子句,可实现8.3.3节所说的循环。
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
在这里,仅当没有引发异常时,才会跳出循环(这是由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语句中的代码可能使用旧式
1
的字符串异常或引发并非从Exception派生而来的异常。然而,如果使用except Exception as e, 就可利用8.3.4节介绍的技巧在这个小型除法程序中打印更有用的错误消息。
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 初始化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
虽然使用del来删除变量是相当愚蠢的清理措施,但finally子句非常适合用于确保文件或网 络套接字等得以关闭,这将在第14章详细介绍。
也可在一条语句中同时包含try、except、finally和else(或其中的3个)。
try:
1 / 0 except NameError:
print("Unknown variable") else:
print("That went well!") finally:
print("Cleaning up.")