Python 中的异常处理机制
在程序运行过程中,总会遇到各种各样的错误,这会使得程序运行结果和预期不符,严重时甚至会导致程序奔溃。
在编码过程中使用合理的 异常处理机制,可以帮助程序在运行出现错误时,捕获并且并处理这些错误,进而尝试恢复程序的执行,或者进行一些程序崩溃前的必要工作(如内存中的数据写入文件等)。异常处理机制对于编写一个良好的、健壮的程序是至关重要的。
异常引入
开发人员在编写程序时,难免会遇到各种各样的错误。
有的错误是编写人员疏忽造成的语法错误,比如缩进错误,语法格式使用错误等,这种错误是必须修复的。
有的错误是程序内部隐含逻辑问题造成的数据错误(调试时很方便检出),这种错误需要仔细分析相应的逻辑流程进行处理。
有的错误是由于用户输入造成的数据错误,比如让用户输入 URL 地址,结果得到一个空字符串,这种错误可以通过检查用户输入来做相应的处理。
还有一类错误是程序运行时与系统的规则冲突造成的系统错误,完全无法在程序运行过程中预测的,比如写入文件的时候,磁盘满了,写不进去了;或者从网络抓取数据,网络突然断掉了等等。这种错误在程序中通常是必须处理的,否则,程序会因为各种问题终止并退出。
总的来说,Python 中编写程序时遇到的错误可大致分为两类:
- 语法错误;
- 运行时错误。
语法错误
语法错误,是指解析代码时出现的错误。
当代码不符合 Python 语法规则时,Python 解释器在解析时就会报出 SyntaxError
语法错误,与此同时还会明确指出最早解析到错误的语句。
例如,Python 3 中已不再支持下面这种写法,运行时,解释器会报错误:
1 | print "Hello,World!" |
除此之外,其它 常见的语法错误类型如下表 >>>>
错误类型 | 含义 |
SyntaxError:unexpected EOF while parsing | 语法错误:多了无法解析的符号,检查是否多了或者少了括号。 |
SyntaxError:invalid character in identifier | 有无效标识符,检查一下是否使用中文符号。 |
SyntaxError -> IndentationError: expected an indented block | 解析时缩进错误:检查一下代码的缩进是否正确。 |
语法错误多是开发者疏忽导致的,属于真正意义上的错误,是解释器无法容忍的,只有将程序中的所有语法错误全部纠正,程序才能执行。
需要注意的是 >>>>
语法错误并不需要异常处理机制的参与,它由开发人员自己保证,我们更关注的是程序运行时产生的错误(异常)。
运行时异常
运行时异常,即程序在语法上都是正确的,但在运行时发生了错误。
实例演示:
1 | a = 1/0 |
可以看到,用 1 除以 0,并赋值给 a 。因为 0 作除数是没有意义的,所以运行后会产生 ZeroDivisionError
错误。在输出结果中,前两段指明了错误的位置,最后一句表示出错的类型。
Python 中,把这种运行时产生错误的情况叫做 异常(Exceptions)。除此以外,Python 中异常情况包括很多种类,常见错误类型接下文。
常见异常类型
Python 中的错误类型也是类,它们都是从 BaseException
类派生的,常见见下表。更多的错误类型和其继承关系请参见:Exception hierarchy。
错误类型 | 具体 | 含义 |
IndexError | list index out of range | 索引错误:数据超出索引范围,检查一下是否数据使用越界。 |
TypeError | must be str, not int … | 数据类型错误:不同类型数据之间的无效操作。比如字符串和数字直接拼接,此时检查一下数据类型是否使用错误。 |
KeyError | KeyError:’fond’ | 键错误:字典中没有该的 key(“fond”)对应的值,检查一下键名或者字典数据是否正确。 |
ValueError | substring not found | 值错误:输入的数据类型跟要求的不符合。 |
NameError | name ‘a’ is not defined | 未初始化对象:变量没有被定义就使用了,或者变量被删除后再次使用。 |
AttributeError | ‘tuple’ object has no attribute ‘remove’ | 对象属性错误:当试图访问的对象属性不存在时抛出的异常 |
SystemExit | SystemExit | 解释器请求退出,出现在 exit() 函数后。 |
IOError | IOError | 输入/输出操作失败 |
ImportError | ModuleNotFoundError | 导入模块/对象失败,检查一下模块是否存在或者能够正常使用。 |
UnicodeError | UnicodeDecodeError / UnicodeEncodeError / UnicodeTranslateError | Unicode 解码/编码/转换/时的错误:请检查字符解码/编码/转换。 |
AssertionError | AssertionError | 当 assert 关键字后的条件为假时,程序运行会停止并抛出 AssertionError 异常 |
引入异常处理机制
当一个程序发生异常时,代表该程序在执行时出现了非正常的情况,无法再执行下去。默认情况下,程序是要终止的。
什么?!!你说你编写的程序不会出错?
请记住,无论你是多么优秀的程序员,你都不能保证自己的程序永远不会出错。就算你的程序没有错,用户也不一定按照你设定的规则来使用你的程序,总有一些小白或者极客会“玩弄”你的程序。
除此以外,你也不能保证程序的运行环境永远稳定,比如网络可能无法连接,磁盘写满 ……
总之,你只可以保证你的程序语法正确(?),其它的…
但是,作为一个负责任的程序员,我们要让自己的程序尽可能的健壮 >>>> 代码要有异常处理
即,当程序运行过程中异常产生后,可以使用捕获异常的方式获取这个异常的名称,再通过其他的逻辑代码尝试恢复程序的执行,或者进行一些程序崩溃前的必要工作,这种根据异常做出相应逻辑处理的过程叫作 异常处理。
自然而然的引发一个问题 >>>>
那么,应该如何捕获和处理异常呢???
幸运的时,所有的高级语言通常都内置了一套 try...except...finally...
用于捕获以及处理异常的 异常处理机制,Python 当然也不例外。
异常处理机制是现代编程语言不可或缺的能力,它已经成为衡量一门编程语言是否成熟和健壮的标准之一。
Python 中的异常处理机制
先来看 Python 中的基本异常处理结构:try except
语句块,它是 Python 异常处理机制的核心结构。
try except 异常处理
Python 中,使用 try except 语句块捕获并处理异常的基本语法结构如下:
1 | try: |
[1] >>>> 语法结构说明
关于 try except 语法结构说明分为两个部分:结构说明 && 参数说明。
1)–> 结构说明
try except 结构中,try 块有且仅有一个,但 except 代码块可以有多个(>=0),并且每个 except 块都可以同时处理多种异常。
注意,except 代码块至少要有一个(没有 finally 块时,见后文),否则会产生语法错误(SyntaxError):
1 | # SyntaxError: invalid syntax |
2)–> 参数说明
[]
括起来的参数部分可以选择使用,也可以省略。其中各部分的含义如下:
- (Error1, Error2,…) 、(Error3, Error4,…):其中,Error1、Error2、Error3 和 Error4 都是具体的异常类型(参见常见异常类型)。显然,一个 except 块可以同时处理多种异常;
- [as e]:作为可选参数,表示给异常类型起一个别名 e,这样做的好处是方便在 except 块中调用异常类型;
- [Exception]:作为可选参数,可以代指程序可能发生的所有异常情况,其通常用在最后一个 except 块。
[2] >>>> 执行流程说明
1)首先执行 try 中的代码块,如果执行过程中出现异常,系统会自动解析生成一个对应异常类型,并将该异常提交给 Python 解释器,此过程称为捕获异常。
2)当 Python 解释器收到异常对象时,会寻找能处理该异常对象的 except 块(取决于 except 语句中异常类型参数),如果找到合适的 except 块,则把该异常对象交给该 except 块处理,这个过程被称为处理异常。如果 Python 解释器找不到处理异常的 except 块,则程序运行终止,Python 解释器也将退出。
注意,如果此段程序没有用 try 包裹,又或者没有为该异常配置处理它的 except 块,则 Python 解释器将无法处理,程序就会停止运行;反之,不管程序代码块是否处于 try 块中,甚至包括 except 块中的代码,只要执行该代码块时出现了异常,系统都会自动解析生成对应类型的异常,捕获后由相应 except 处理完成,则程序可以继续执行。
来看一个实例:
1 | print("Begin To Run") |
程序运行结果为:
1 | Begin To Run |
可以看到,第一个 except 块使用(ValueError, ArithmeticError)来指定可以处理的异常类型,这就表明该 except 块可以同时处理这 2 种类型的异常;第二个 except 块使用省略异常类的 except 语句(except Exception),它并未指定具体要处理的异常类型,表示可处理所有类型的异常,一般会作为异处理的最后一个 except 块。
上面,由于 try 块中引发了异常,并被 Python 解释器捕获,并找到第一个 except 块可处理相应异常,处理后,程序继续执行,输出 Continue To Run
。
[3] >>>> 获取发生的特定异常相关信息
上面,我们已经可以捕获程序中可能发生的异常,并对其进行处理。
但是,由于一个 except 可以同时处理多个异常,那么我们如何知道当前处理的到底是哪种类型的异常呢?
事实上,每种异常类型都具有了如下几个属性和方法,通过调用它们,就可以获取当前处理异常类型的相关信息:
- args:返回异常信息的描述字符串;
- str(e):返回异常信息,但不包括异常信息的类型;
- repr(e):返回较全的异常信息,包括异常信息的类型。
基于此,我们可以给捕获到的异常类型起一个别名 e(as e),就可以访问其属性输出相应异常信息了:
1 | try: |
从程序中可以看到,由于 except 可能接收多种异常,因此为了操作方便,直接给每一个进入到此 except 块的异常,起一个统一的别名 e。
注意,在 Python 2.x 的早期版本中,除了使用
as e
这个格式,还可以将其中的 as 用逗号,
代替(即 exception Exception, as)。
深入解读异常处理块查找机制
我们知道,当位于 try 块中的程序执行出现异常时,会将该种异常捕获,同时找到对应的 except 块处理该异常。
那么这里就有一个问题,Python 解释器是如何找到对应的 except 块的呢???
[1] >>>> 异常类
我们知道,一个 try 块也可以对应多个 except 块,一个 except 块可以同时处理多种异常,并且如果我们想使用一个 except 块处理所有异常还可以使用省略异常类型的 except 关键字(或 Exception)。
你肯定困惑过,为什么 Exception 异常可以对应所有的异常处理???这就不得不提到异常类了。
事实上,异常也是类。为了表示程序中可能出现的各种异常,Python 提供了大量的异常类,这些异常类之间有严格的继承关系。如下(异常类详情请参见 Exception Hierarchy):
可以看出,BaseException 是 Python 中所有异常类的基类,但对于我们来说,最主要的是 Exception 类,因为程序中可能出现的各种异常,都继承自 Exception。
了解了异常类以及其继承关系之后,就可以开始解读异常处理块的查找机制了 >>>>
[2] >>>> 查找机制
当 try 块捕获到异常对象后,Python 解释器会拿这个异常类型依次和各个 except 块指定的异常类进行比较,如果捕获到的这个异常类,和某个 except 块后的异常类一样,又或者是该异常类的子类,那么 Python 解释器就会调用这个 except 块来处理异常。
反之,Python 解释器会继续比较,直到和最后一个 except 比较完,如果没有比对成功,则证明该异常无法处理。
异常处理块查找机制示意图如下所示:
简单的异常捕获的例子:
1 | try: |
该程序中,根据用户输入 a 和 b 值的不同,可能会导致 ValueError、ArithmeticError 异常:
- 如果用户输入的 a 或者 b 是其他字符,而不是数字,会发生 ValueError 异常,try 块会捕获到该类型异常,同时 Python 解释器会调用第一个 except 块处理异常;
- 如果用户输入的 a 和 b 是数字,但 b 的值为 0,由于在进行除法运算时除数不能为 0,因此会发生 ArithmeticError 异常,try 块会捕获该异常,同时 Python 解释器会调用第二个 except 块处理异常;
- 当然,程序运行过程中,还可能由于其他因素出现异常,try 块都可以捕获,同时 Python 会调用最后一个 except 块来处理。
[3] >>>> except 块异常类型定义规则
当一个 try 块配有多个 except 块时,这些 except 块应遵循这样一个排序规则,即可处理全部异常的 except 块(参数为 Exception,也可以省略)要放到所有 except 块的后面
并且,所有父类异常的 except 块要放到子类异常的 except 块的后面。这意味着,一旦父类放于前面,它不但捕获该类型的错误,还把其子类也“一网打尽”,会导致其子类的 except 块永远也无法执行到(无意义的 except 块)。
例如:
1 | try: |
第二个 except 永远也处理 UnicodeError 异常,因为 UnicodeError 是 ValueError 的子类,如果有,也被第一个 except 给处理了。
try except 通用形式
我们知道,try except 语句块结构是 Python 异常处理机制中的核心结构。
但在实际使用过程中,还可以根据实际需要在 try except 语句块结构基础上添加 else 块和 finally 块结构(都是可选的),这样就变为:
- try except else 语句块结构
- try except finally 语句块结构
- try except else finally 语句块结构
try except else 异常处理
Python 异常处理还提供了一个 else 机制,也就是原有 try except 语句的基础上再添加一个 else 块,即 try except else 结构。
try except else 语句块结构如下:
1 | try: |
注意:使用 else 包裹的代码,只有当 try 块没有捕获到任何异常时,才会得到执行;反之,如果 try 块捕获到异常,即便调用对应的 except 处理完异常,else 块中的代码也不会得到执行。
实例如下:
1 | try: |
你可能会困惑,既然 Python 解释器按照顺序执行代码,那么 else 块有什么存在的必要呢?直接将 else 块中的代码编写在 try except 块的后面,不是一样吗?
事实上,else 的功能,只有当 try 块捕获到异常时才能显现出来。在这种情况下,else 块中的代码不会得到执行的机会。如下运行:
1 | # 请输入除数: "2" |
try except finally 异常处理
Python 异常处理机制还提供了一个 finally 语句,通常用来为 try 块中的程序做扫尾清理工作。
try except finally 基本结构如下:
1 | try: |
结构说明:和 else 语句不同,finally 只要求和 try 搭配使用,而至于该结构中是否包含 except 以及 else,对于 finally 都不是必须的(else 必须和 try except 搭配使用)。
在整个异常处理机制中,finally 语句块的功能是:无论 try 块是否发生异常,最终都要进入 finally 语句,并执行其中的代码块。
[1] >>>> finally 语句块作用
Python 垃圾回收机制,只能帮我们回收变量、类对象、函数等占用的内存,而无法自动完成类似关闭文件、数据库连接等这些的工作。
基于上述 finally 语句块的特性,在某些情况下,当 try 块中的程序打开了一些物理资源(文件、数据库连接等)时,由于这些资源必须手动回收,而回收工作通常就可以放在 finally 块中。
当然了,回收这些物理资源并不是必须使用 finally 块,但使用 finally 块是比较好的选择。
这是由于,try 块不适合做资源回收工作,因为一旦 try 块中的某行代码发生异常,则其后续的代码将不会得到执行;其次 except 和 else 也不适合,它们都可能不会得到执行;而 finally 块中的代码,无论 try 块是否发生异常,该块中的代码都会被执行。
[2] >>>> 演示实例
1 | try: |
1)–> 正常运行此程序:
1 | 请输入 a 的值: 4 |
可以看到,当 try 块中代码未发生异常时,except 块不会执行,else 块和 finally 块中的代码会被执行。
2)–> 运行中产生异常:
1 | 请输入 a 的值: 0 |
可以看到,当 try 块中代码发生异常时,except 块得到执行,而 else 块中的代码将不执行,finally 块中的代码仍然会被执行。
3)–> 程序异常退出情况
finally 块的强大还远不止此,即便当 try 块发生异常,且没有合适和 except 处理异常时,finally 块中的代码也会得到执行。例如:
1 | try: |
可以看到,当 try 块中代码发生异常,导致程序崩溃时,在崩溃前 Python 解释器也会执行 finally 块中的代码。
try except else finally 异常处理通用形式
Python 通用的异常处理语法结构如下:
1 | try: |
异常处理结构流程图如下:
整个异常处理结构中,只有 try 块是必需的,也就是说:
- 如果没有 try 块,则不能有后面的 except 块、else 块和 finally 块。但是也不能只使用 try 块,要么使用 try except 结构,要么使用 try finally 结构;
- except 块、else 块、finally 块都是可选的,当然也可以同时出现
- 可以有多个 except 块,但捕获父类异常的 except 块应该位于捕获子类异常的 except 块的后面;
- 多个 except 块必须位于 try 块之后,finally 块必须位于所有的 except 块之后。
- 要使用 else 块,其前面必须包含 try 和 except。
程序退出情况下 finally 运行说明 >>>>
[1] >>>> break、continue、return 退出情况
finally 语句不管异常是否发生都会执行。不仅如此,无论是正常退出、遇到异常退出,还是通过 break、continue、return 语句退出,finally 语句块都会执行。
[2] >>>> 解释器退出(os._exit(1))情况
如果 try 块、except 块中调用了退出 Python 解释器的方法,则 finally 语句将无法得到执行。否则不管在 try 块、except 块中执行怎样的代码,出现怎样的情况,异常处理的 finally 块总会被执行。
实例演示:
1 | import os |
运行程序,没有任何输出。
[3] >>>> return、raise 中止语句
尽量避免在 finally 块里使用 return 或 raise 等导致方法中止的语句,它将会导致 try 块、except 块中的 return、raise 语句失效。如下:
1 | def test(): |
仔细思考一下,无论是否产生异常(不考虑解释器退出情况),finally 块均会执行。此时如果,try 块、except 块中包含 return、raise,而 finally 块也包含相应 return、raise 中止语句时,解释器到底该返回哪一个???
如果 Python 程序在执行 try 块、except 块包含有 return 或 raise 语句,则 Python 解释器执行到该语句时,会先去查找 finally 块,如果没有 finally 块,程序才会立即执行 return 或 raise 语句。
反之,如果找到 finally 块,系统立即开始执行 finally 块,只有当 finally 块执行完成后,系统才会再次跳回来执行 try 块、except 块里的 return 或 raise 语句。如果此时 finally 块里也使用了 return 或 raise 等导致方法中止的语句,finally 块己经中止了方法,系统将不会跳回去执行 try 块、except 块里的任何代码。
Raise 手动抛出异常
raise 语句的基本语法格式为:
1 | raise [exceptionName [(reason)]] |
说明,用 []
括起来的为可选参数,其作用是指定抛出的异常类型的名称,以及相应异常的描述信息。如果可选参数全部省略,则 raise 会把当前 Python 解释器检测到的上下文错误原样抛出;如果仅省略 (reason),则在抛出指定异常时,将不附带任何的异常描述信息。
也就是说,raise 语句有如下三种常用的用法:
- raise:可选参数全部缺省。该语句会把当前 Python 解释器自动检测到的上下文错误原样抛出,如没有检测到其它上下文异常则默认引发
RuntimeError
异常; - raise exceptionName:raise 后带一个异常类型名称,表示手动抛出一个指定的
exceptionName
类型的异常(该指定异常是 Python 内置的异常类型或用户自定义异常); - raise exceptionName(reason):在抛出指定类型异常的同时,附带异常的描述信息。
你可能会感到非常困惑,我们都是想方设法地让程序正常运行,为什么还要手动抛出异常呢???
通常情况下,手动让程序引发异常,很多时候并不是为了让其崩溃,而是针对程序运行中可能出现的异常进行手动捕获并处理。事实上,raise 语句引发的异常通常结合 try except(else finally)异常处理结构来捕获并进行处理。
实例演示:
1 | while True: |
程序执行时,当用户输入的不是数字时,程序会进入 if 判断语句,并执行 raise 引发 ValueError 异常。但由于其位于 try 块中,因为 raise 抛出的异常会被 try 捕获,并由 except 块进行处理。
可以看到,虽然程序中使用了 raise 语句引发异常,但程序的执行是正常的,手动抛出的异常并不会导致程序崩溃。
无参 raise >>>>
1)–> 上下文中已引发过异常
1 | try: |
这里重点关注位于 except 块中的 raise,由于在其之前我们已经手动引发了 ValueError 异常,因此这里当再使用 raise 语句时,它会再次引发一次。
2)–> 上下文中未引发过异常
需要注意的是,在没有引发过异常的程序使用无参的 raise 语句时,它默认引发的是 RuntimeError 异常。例如:
1 | try: |
install_url
to use ShareThis. Please set it in _config.yml
.