@[toc]
编写计算机程序时,通常能够区分正常和异常情况。异常事件可能是错误,也可能是通常不会发生的事情。为处理这些异常事件,Python提供功能强大的异常处理机制。
一、异常是什么
Python使用异常对象来表示异常状态,并在遇到错误时引发异常。异常对象未被处理(或捕获)时,程序将终止并显示一条错误消息(traceback
)。
>>> 1/0
Traceback (most recent call last):
File "<pyshell#12>", line 1, in <module>
1/0
ZeroDivisionError: division by zero
事实上,每个异常都是某个类(这里是ZeroDivisionError
)的实例。你能以各种方式引发和捕获这些实例,从而逮住错误并采取措施,而不是让程序失败
二、让事情沿你指定的轨道出错
正如你看到的,出现问题时,将自动引发异常。先来看看如何自主地引发异常,还有如何创建异常,然后再学习如何处理这些异常。
1. raise语句
Python使用raise语句
抛出一个指定异常。要引发异常,可使用raise语句
,并将一个类(必须是Exception
的子类)或实例作为参数。将类作为参数时,将自动创建一个实例。下面的示例使用的是内置异常类Exception
:
>>> raise Exception
Traceback (most recent call last):
File "<pyshell#13>", line 1, in <module>
raise Exception
Exception
这里引发的是通用异常,并没有指出出现了什么错误。
>>> raise Exception('hyperdrive overload')
Traceback (most recent call last):
File "<pyshell#14>", line 1, in <module>
raise Exception('hyperdrive overload')
Exception: hyperdrive overload
这里添加了错误信息hyperdrive overload
。
在很多内置的异常类,表描述了最重要的几个。
<center>一些内置的异常类</center>
类名 | 描述 |
---|---|
Exception | 几乎所有的异常类都是从它派生而来的 |
AttributeError | 引用属性或给它赋值失败时引发 |
OSError | 操作系统不能执行指定的任务(如打开文件)时引发,有多个子类 |
IndexError | 使用序列中不存在的索引时引发,为LookupError的子类 |
KeyError | 使用映射中不存在的键时引发,为LookupError的子类 |
NameError | 找不到名称(变量)时引发 |
SyntaxError | 代码不正确时引发 |
TypeError | 将内置操作或函数用于类型不正确的对象时引发 |
ValueError | 将内置操作或函数用于这样的对象时引发:其类型正确但包含的值不合适 |
ZeroDivisionError | 在除法或求模运算的第二个参数为零时引发 |
2. 自定义的异常类
我们可以像创建其他类一样,但务必直接或间接地继承Exception
(这意味着从任何内置异常类派生都可以)。因此,自定义异常类的代码类似于下面这样:
class SomeCustomException(Exception): pass
因为错误就是类,捕获一个错误就是捕获一个该类的实例,因此错误并不是凭空产生的,而是由一些不合理的部分导致的。Python的内置函数会抛出很多类型的错误,我们自己编写的函数也可以抛出错误。如果要抛出错误,那么可以根据需要定义一个错误的类,选择好继承关系,然后用raise语句
抛出一个错误的实例。
例如:
class MyError(Exception):
def __init__(self):
pass
def __str__(self):
return 'this is self define error'
def my_error_test():
try:
raise MyError()
except MyError as e:
print('exception info:',e)
my_error_test()
执行结果如下:
exception info:this is self define error
程序正确地执行了自定义的异常。
三、捕获异常
异常比较有趣的地方是可对其进行处理,通常称之为捕获异常。为此,可使用try/except语句
。try/except语句
用来检测try语句中的错误,从而让except语句
捕获异常信息并处理。假设你创建了一个程序,让用户输入两个数,再将它们相除,如下所示:
>>> x = int(input('Enter the first number:'))
>>> y = int(input('Enter the second number'))
>>> print(x / y)
如果第二个数字为零的话,就会报错。为了捕获这种异常并对错误进行处理,可像下面这样重写程序。
try:
x = int(input('Enter the first number:'))
y = int(input('Enter the second number:'))
print(x / y)
except ZeroDivisionError:
print("第二个数字不能为0")
try
的工作原理是,开始一个try语句
后,Python就在当前程序的上下文中做标记,当出现异常时就可以回到做标记的地方。首先执行try语句
,接下来发生什么依赖于执行时是否出现异常。
如果try
后的语句执行时发生异常,程序就跳回try并执行except子句
。异常处理完毕,控制流就可以通过整个try语句
了(除非在处理异常时又引发新异常)。
注意:异常从函数向外传播到调用函数的地方。如果在这里也没有被捕获,异常将向程序的最顶层传播。这意味着你可使用try/except来捕获他人所编写函数引发的异常。详见第4小节。
1. 不用提供参数
捕获异常后,如果要重新引发它(即继续向上传播),可调用raise
且不提供任何参数(也可显式地提供捕获到的异常,参见(4))。
>>> raise
Traceback (most recent call last):
File "<pyshell#31>", line 1, in <module>
raise
RuntimeError: No active exception to reraise
为说明这很有用,来看一个能够抑制异常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
# eval()将字符串str当成有效的表达式来求值并返回计算结果
注意:发生除零行为时,如果启用了抑制功能,方法calc
将(隐式地)返回None。换而言之,如果启用了抑制功能,就不应依赖返回值。
下面的示例演示了这个类的用法(包括启用和关闭了抑制功能的情形):
>>> calculator = MuffledCalculator()
>>> calculator.calc('10/2')
5.0
>>> calculator.calc('10/0') #关闭了抑制功能
Traceback (most recent call last):
File "<pyshell#18>", line 1, in <module>
calculator.calc('10/0')
File "<pyshell#15>", line 5, in calc
return eval(expr)
File "<string>", line 1, in <module>
ZeroDivisionError: division 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 "<pyshell#28>", line 2, in <module>
1/0
ZeroDivisionError: division by zero
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<pyshell#28>", line 4, in <module>
raise ValueError
你可使用raise…from…
语句来提供自己的异常上下文,也可使用None来禁用上下文。
>>> try:
1/0
except ZeroDivisionError:
raise ValueError from None
Traceback (most recent call last):
File "<pyshell#30>", line 4, in <module>
raise ValueError from None
ValueError
2. 多个except子句
如果你运行前一节的程序,并在提示时输入一个非数字值,将引发另一种异常。
Enter the first number: 10
Enter the second number: 'Hello'
Traceback (most recent call last):
File "<pyshell#33>", line 3, in <module>
y = int(input('Enter the second number'))
ValueError: invalid literal for int() with base 10: "'hello'"
由于该程序中的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("第二个数字不能为0")
except ValueError:
print("这不是一个数字")
try语句按照如下方式工作:
首先,执行try子句(在关键字try和关键字except之间的语句)。
如果没有发生异常,忽略except子句,try子句执行后结束。如果在执行try子句的过程中发生异常,try子句余下的部分会被忽略。如果异常的类型和except之后的名称相符,对应的except子句就会被执行。最后执行try语句之后的代码。如果没有一个异常与任何except匹配,这个异常就会传递到上层的try中。一个try语句可能包含多个except子句,分别处理不同的异常,但最多只有一个分支会被执行。
处理程序将只针对对应try子句
中的异常进行处理,而不会处理其他异常语句中的异常。
注意到异常处理并不会导致代码混乱,而添加大量的if语句来检查各种可能的错误状态将导致代码的可读性极差。
3. 一箭双雕
如果要使用一个except子句
捕获多种异常,可在一个元组中指定这些异常,如下所示:
try:
x = int(input('Enter the first number:'))
y = int(input('Enter the second number:'))
print(x / y)
except (ZeroDivisionError, TypeError, ValueError, NameError):
print("你的数字是伪造的")
在上述代码中,如果用户输入字符串、其他非数字值或输入的第二个数为零,都将打印同样的错误消息。当然,仅仅打印错误消息帮助不大。另一种解决方案是不断地要求用户输入数字,直到能够执行除法运算为止,(6)将介绍如何这样做。
这样做有什么好处呢?假如我们希望多个except子句
输出同样的信息,就没有必要在几个except子句
中重复输入语句,放到一个异常块中即可。
在except子句
中,异常两边的圆括号很重要。一种常见的错误是省略这些括号,这可能导致你不想要的结果,其中的原因请参阅下一节。
4. 捕获对象
要在except子句
中访问异常对象本身,可使用两个而不是一个参数。(请注意,即便是在你捕获多个异常时,也只向except
提供了一个参数—一个元组。)需要让程序继续运行并记录错误(可能只是向用户显示)时,这很有用。如果希望在except子句
中访问异常对象本身,也就是看到一个异常对象真正的异常信息,而不是输出自己定义的异常信息,可以使用as e
的形式,我们称之为捕捉对象。
下面的示例程序打印发生的异常并继续运行:
try:
x = int(input('Enter the first number:'))
y = int(input('Enter the second number:'))
print(x / y)
except (ZeroDivisionError, ValueError) as e:
print(e)
#division by zero
#invalid literal for int() with base 10: "'s'"
在这个小程序中,except子句
也捕获两种异常,但由于你同时显式地捕获了对象本身,因此可将其打印出来,让用户知道发生了什么情况。(6)将介绍这种技术的另一种更有用的用途。
5. 一网打尽
即使程序处理了好几种异常,还是可能有一些漏网之鱼。例如,对于前面执行除法运算的程序,如果用户在提示时不输入任何内容就按回车键,将出现一条错误消息,还有一些相关问题出在什么地方的信息(栈跟踪),如下所示:
Traceback (most recent call last):
…
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 …')
现在,用户输入什么错误信息都可以报错。可以在except子句
中忽略所有异常类,从而让程序输出自己定义的异常信息。
但是,像这样捕获所有的异常很危险,因为这不仅会隐藏你有心理准备的错误,还会隐藏你没有考虑过的错误。这还将捕获用户使用Ctrl + C
终止执行的企图、调用函数sys.exit
来终止执行的企图等。在大多数情况下,更好的选择是使用except Exception as e
并对异常对象进行检查。这样做将让不是从Exception
派生而来的为数不多的异常成为漏网之鱼,其中包括SystemExit
和KeyboardInterrupt
,因为它们是从BaseException(Exception的超类)
派生而来的。
6. 万事大吉时
在有些情况下,在没有出现异常时执行一个代码块很有用。为此,可像条件语句和循环一样,给try/except语句
添加一个else子句
。
try:
print('A simple tasl')
except:
print('what? Something went wrong?')
else:
print('Ah … It went as planned.')
如果你运行这些代码,输出如下:
A simple tasl
Ah … It went as planned.
通过使用else子句
,可实现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
在这里,仅当没有引发异常时,才会跳出循环。换而言之,只要出现错误,程序就会要求用户提供新的输入。
前面说过,一种更佳的替代方案是使用空的except子句
来捕获所有属于类Exception
的异常。你不能完全确定这将捕获所有的异常,因为try/except语句
中的代码可能使用旧式的字符串异常或引发并非从Exception
派生而来的异常。然而,如果使用except Exception as e
,就可利用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
如果在try子句
执行时没有发生异常,就会执行else语句
后面的语句。使用else子句
比把所有语句都放在try子句
里更好,这样可以避免一些意想不到而except
又没有捕获的异常。
当程序没有发生异常时,通过添加一个else子句
做一些事情(比如输出一些信息)很有用,可以帮助我们更好地判断程序的执行情况。
7. 最后
最后,还有finally子句
,可用于在发生异常时执行清理工作。这个子句是与try子句
配套的,try/finally语句
无论发生异常与否都将执行最后的代码。
x = None
try:
x = 1 / 0
finally:
print('Cleaning up!不管怎么样我都会先展示在最前面')
del x
在上述示例中,finally子句
被执行了,不管try子句
中发生什么异常,都将执行finally子句
。为何在try子句
之前初始化x
呢?因为如果不这样做,ZeroDivisionError
将导致根本没有机会给它赋值,进而导致在finally子句
中对其执行del
时引发未捕获的异常。
如果运行这个程序,它将在执行清理工作后崩溃。
Cleaning up! 不管怎么样我都会先展示在最前面
Traceback (most recent call last):
File "<pyshell#42>", line 2, in <module>
x = 1 / 0
ZeroDivisionError: division by zero
虽然使用del
来删除变量是相当愚蠢的清理措施,但finally子句
非常适合用于确保文件或网络套接字等得以关闭,这将在第14章介绍。
也可在一条语句中同时包含try
、except
、finally
和else
(或其中的3个),但要记得else
在except
之后,finally在except
和else
之后。
try:
1/0
except ZeroDivisionError:
print('Division by zero')
else:
print('That went well')
finally:
print('cleaning up.')
#Division by zero
#cleaning up.
finally子句在关闭文件或数据库连接时非常有用。
8. 补充和总结
注意:如果抛出父类异常,在子类不会再获取。
如下:
try:
fun()
except Exception as e:
raise Exception
except ImportError as e:
raise ImportError
finally:
pass
在上面的例子中,下面的ImportError
就不会被抛出,应为ImportError
继承Exception
,但是可以把Exception
放在后面是可以的
try:
suite1 #测试语句块
except exception1:
suite2 #如果测试语句suite1中发生exception1异常时执行
except (exception2,exception3):
suite3 #如果测试语句suite1中发生元组中任意异常时执行
except exception4 as reason: #as把异常的原因赋值给reason
suite4 #如果测试语句suite1发生exception4的异常时执行
except:
suite5 #如果测试语句suite1发生异常在所列出的异常之外时执行
else:
suite5 #如果测试语句块suite1中没有发生异常时执行
finally:
suit6 #不管测试语句suite1中又没有发生异常都会执行
所有错误类型都继承自BaseException
四、异常和函数
异常和函数有着天然的联系。如果不处理函数中引发的异常,它将向上传播到调用函数的地方。如果在那里也未得到处理,异常将继续传播,直至到达主程序(全局作用域)。如果主程序中也没有异常处理程序,程序将终止并显示栈跟踪消息。来看一个示例。
>>> def faulty():
raise Exception('Something is wrong')
>>> def ignore_exception():
faulty()
>>> def handle_exception():
try:
faulty()
except:
print('Exception handled')
>>> ignore_exception()
结果如下
Traceback (most recent call last):
File "C:/Users/MIC/Desktop/test1.py", line 11, in <module>
ignore_exception()
File "C:/Users/MIC/Desktop/test1.py", line 4, in ignore_exception
faulty()
File "C:/Users/MIC/Desktop/test1.py", line 2, in faulty
raise Exception('Something is wrong')
Exception: Something is wrong
>>> handle_exception()
Exception handled
如你所见,faulty
中引发的异常依次从faulty
和ignore_exception
向外传播,最终导致显示一条栈跟踪消息。调用handle_exception
时,异常最终传播到handle_exception
,并被这里的try/except语句
处理。
注意:异常信息是以堆栈的形式被抛出的,因而是从下往上查看的。所谓堆栈,就是最先被发现的异常信息最后被输出,也被称作先进后出。
五、异常之禅
异常处理并不是很复杂。如果你知道代码可能引发某种异常,且不希望出现这种异常时程序终止并显示站跟踪消息,可添加必要的try/except或try/finally语句(或结合使用)来处理它。
有时候,可使用条件语句来达成异常处理实现的目标,但这样编写出来的代码可能不那么自然,可读性也没那么高。另一方面,有些任务使用if/else完成时看似很自然,但实际上使用try/except来完成要好得多。下面来看两个示例。
假设有一个字典,你要在指定的键存在时打印与之相关联的值,否则什么都不做。实现这种功能的代码可能类似于下面这样:
def describe_person(person):
print('Description of', person['name'])
print('Age:', person['age'])
if 'occupation' in person;
print('Occupation:', person['occupation'])
如果你调用这个函数,并向它提供一个包含姓名Throatwobbler Mangrove
和年龄42的字典,输出将如下:
Description of Throatwobbler Mangrove
Age: 42
如果你在这个字典中添加职业camper
,输出将如下:
Description of Throatwobbler Mangrove
Age: 42
Occupation: camper
这段代码很直观,但效率不高,因为它必须两次查找'Occupation'
键:一次检查这个键是否存在(在条件中),另一次获取这个键关联的值,以便将其打印出来。下面是另一种解决方案:
def describe_person(person):
print('Description of', person['name'])
print('Age:', person['age'])
try:
print('Occupation:', person['occupation'])
except KeyError: pass
在这里,函数直接假设存在'Occupation:'
键。如果这种假设正确,就能省点事:直接获取并打印值,而无需检查这个键是否存在。如果这个键不存在,将引发KeyError
,而except子句
将捕获这个异常。
你可能发现,检查对象是否包含特定的属性时,try/except
也很有用。例如,要检查一个对象是否包含属性write
,可使用类似于下面的代码:
try:
obj.write
except AttributeError:
print('The object is not writeable')
else:
print('The object is writeable')
在这里,try子句
只是访问属性write
,而没有使用它来做任何事情。如果引发了AttributeError异常
,说明对象没有属性write
,否则就说明有这个属性。这种解决方案可代替7.2.9节介绍的使用getattr
的解决方案,而且更自然。具体使用哪种解决方案,在很大程度上取决于个人喜好。
在很多情况下,相比于使用if/else
,使用try/except语句
更自然,也更符合Python的风格。因此你应养成尽可能使用try/except语句
的习惯。
六、不那么异常的情况
如果你只想发出警告,指出情况偏离了正规,可使用模块warnings
中的函数warn
。
>>> from warnings import warn
>>> warn("I've got a bad feeling about this.")
Warning (from warnings module):
File "C:/Users/MIC/Desktop/test1.py", line 1
def faulty():
UserWarning: I've got a bad feeling about this.
警告只显示一次。如果再运行最后一行代码,什么事情都不会发生。
如果其他代码在使用你的模块,可使用模块warning
中的函数filterwarning
来抑制你发出的警告(或特定类型的警告),并指定要采取的措施,如"error"或"ignore"。
>>> from warnings import filterwarnings
>>> filterwarnings("ignore")
>>> warn("Anyone out there?")
>>> filterwarnings("error")
>>> warn("Something is very wrong!")
Traceback (most recent call last):
File "<pyshell#51>", line 1, in <module>
warn("Something is very wrong!")
UserWarning: Something is very wrong!
如你所见,引发的异常为UserWaining
。发出警告时,可指定将引发的异常(即警告类型),但必须是Warning的子类
。如果将警告转换为错误,将使用你指定的异常。另外,还可根据异常来过滤特定类型的警告。
>>> filterwarnings("error")
>>> warn("This function is really old ...", DeprecationWarning) #指定异常
Traceback (most recent call last):
File "<pyshell#54>", line 1, in <module>
warn("This function is really old ...", DeprecationWarning)
DeprecationWarning: This function is really old ...
>>> filterwarnings("ignore", category=DeprecationWarning) #指定过滤掉的异常
>>> warn("Another deprecation warning.", DeprecationWarning)
>>> warn("Something else.")
Traceback (most recent call last):
File "<pyshell#57>", line 1, in <module>
warn("Something else.")
UserWarning: Something else.
除上述基本用途外,模块warnings
还提供了一些高级功能。