程序员的一生中, 错误几乎每天都在发生. 在过去的一个时期, 错误要么对程序(可能还有机器)是致命的, 要么产生一大堆无意义的输出, 无法被其他计算机或程序识别, 连程序远自己也可能搞不懂它的意义. 一旦出现错误, 程序就会终止执行, 直到错误被修正, 程序重新执行. 所以,人们需要一个"柔和"的处理错误的方法, 而不是终止程序. 同时, 程序本身也在不断发展, 并不是每个错误都是致命的, 即使错误发生, 编译器或是在执行中的程序也可以提供更多更有用的诊断信息, 帮助程序员尽快解决问题. 然而, 错误毕竟是错误, 一般都是停止编译或执行后才能去解决它. 一小段代码只能让程序终止执行, 也许还能打印出一些模糊的提示. 当然, 这一切都是在异常和异常处理出现之前的事了.
12.1 什么是异常
12.1.1 错误
在深入介绍异常之前, 我们来看看什么是错误. 从软件方面来说, 错误是语法或是逻辑上的.
语法错误指示软件的结构上有错误, 导致不能被解释器解释或编译器无法编译. 这些错误必须在程序执行前纠正.
当程序的语法正确后, 剩下的就是逻辑错误了. 逻辑错误可能是由于不完整或是不合法的输入所致; 在其他情况下, 还可能是逻辑无法生成, 计算, 或是输出结果需要的过程无法执行. 这些错误通常分别被称为域错误和范围错误.
当 Python 检测到一个错误时, 解释器就会指出当前流已经无法继续执行下去. 这时候就出现了异常.
12.1.2 异常
对异常的最好描述是: 它是因为程序出现了错误而在正常控制流以外采取的行为. 这个行为又分为两个阶段:
首先是引起异常发生的错误,然后是检测(和采取可能的措施)阶段.
- 第一个阶段是在发生了一个异常条件(有时候也叫做例外的条件)后发生的. 只要检测到错误并且意识到异常条件, 解释器会引发一个异常. 引发也可以叫做触发, 引发或者生成. 解释器通过它通知当前控制流有错误发生. Python 也允许程序员自己引发异常. 无论是 Python 解释器还是程序员引发的, 异常就是错误发生的信号.
- 第二个阶段是当前流将被打断, 用来处理这个错误并采取相应的操作.
对异常的处理发生在第二阶段. 异常引发后, 可以调用很多不同的操作. 可以是忽略错误(记录错误但不采取任何措施, 采取补救措施后终止程序), 或是减轻问题的影响后设法继续执行程序.所有的这些操作都代表一种继续, 或是控制的分支. 关键是程序员在错误发生时可以指示程序如何执行.
你可能已经得出这样一个结论: 程序运行时发生的错误主要是由于外部原因引起的, 例如非法输入或是其他操作失败等等. 这些因素并不在程序员的直接控制下, 而程序员只能预见一部分错误,编写常见的补救措施代码
12.1.3 Python 中的异常
Python 内建异常
异常名称 | 描述 |
---|---|
*BaseException | 所有异常的基类 |
SystemExit | python 解释器请求退出 |
KeyboardInterrupt | 用户中断执行(通常是输入^C) |
*Exception | 常规错误的基类 |
*StopIteration | 迭代器没有更多的值 |
GeneratorExit | 生成器(generator)发生异常来通知退出 |
StandardError | 所有的内建标准异常的基类 |
ArithmeticError | 所有数值计算错误的基类 |
FloatingPointError | 浮点计算错误 |
OverflowError | 数值运算超出最大限制 |
*ZeroDivisionError | 除(或取模)零 (所有数据类型) |
AssertionError | 断言语句失败 |
AttributeError | 对象没有这个属性 |
EOFError | 没有内建输入,到达 EOF 标记 |
EnvironmentError | 操作系统错误的基类 |
*IOError | 输入/输出操作失败 |
OSError | 操作系统错误 |
WindowsError | Windows 系统调用失败 |
ImportError | 导入模块/对象失败 |
KeyboardInterrupt | 用户中断执行(通常是输入^C) |
LookupError | 无效数据查询的基类 |
IndexError | 序列中没有没有此索引(index) |
KeyError | 映射中没有这个键 |
MemoryError | 内存溢出错误(对于 Python 解释器不是致命的) |
*NameError | 未声明/初始化对象 (没有属性) |
UnboundLocalErrorh | 访问未初始化的本地变量 |
ReferenceError | 弱引用(Weak reference)试图访问已经垃圾回收了的对象 |
RuntimeError | 一般的运行时错误 |
NotImplementedError | 尚未实现的方法 |
*SyntaxError | Python 语法错误 |
IndentationError | 缩进错误 |
TabError | Tab 和空格混用 |
SystemError | 一般的解释器系统错误 |
*TypeError | 对类型无效的操作 |
ValueError | 传入无效的参数 |
UnicodeError | Unicode 相关的错误 |
UnicodeDecodeError | Unicode 解码时的错误 |
UnicodeEncodeError | Unicode 编码时错误 |
UnicodeTranslateError | Unicode 转换时错误 |
Warning | 警告的基类 |
DeprecationWarning | 关于被弃用的特征的警告 |
FutureWarning | 关于构造将来语义会有改变的警告 |
OverflowWarning | 旧的关于自动提升为长整型(long)的警告 |
PendingDeprecationWarning | 关于特性将会被废弃的警告 |
RuntimeWarning | 可疑的运行时行为(runtime behavior)的警告 |
SyntaxWarning | 可疑的语法的警告 |
UserWarning | 用户代码生成的警告 |
12.2 检测和处理异常
异常可以通过 try 语句来检测. 任何在 try 语句块里的代码都会被监测, 检查有无异常发生.
try 语句有两种主要形式: try-except
和 try-finally
. 这两个语句是互斥的, 也就是说你只能使用其中的一种.
一个try语句可以对应一个或多个except子句,但只能对应一个finally子句,或是一个try-except-finally复合语句。
你可以使用 try-except 语句检测和处理异常.
你也可以添加一个可选的 else 子句处理没有探测到异常的时执行的代码.
try-finally 只允许检测异常并做一些必要的清除工作(无论发生错误与否), 没有任何异常处理设施.
正如你想像的,复合语句两者都可以做到.
12.2.1 try-except 语句
try-except 语句(以及其更复杂的形式)定义了进行异常监控的一段代码, 并且提供了处理异常的机制.
最常见的try-except语句语法如下所示.
try:
try_suite
# watch for exceptions here 监控这里的异常
except Exception[, reason]:
except_suite
# exception-handling code 异常处理代码
它由try块和except块(try_suite和except_suite)组成,也可以有一个可选的错误原因.
>>> try:
... f = open('blah', 'r')
... except IOError, e:
... print 'could not open file:', e
... could not open file: [Errno 2] No such file or directory
12.2.2 核心笔记: 忽略代码, 继续执行, 和向上移交
try 语句块中异常发生点后的剩余语句永远不会到达(所以也永远不会执行).
- 一旦一个异常被引发, 就必须决定控制流下一步到达的位置. 剩余代码将被忽略, 解释器将搜索处理器(异常处理器), 一旦找到,就开始执行处理器中的代码.
- 如果没有找到合适的处理器, 那么异常就向上移交给调用者去处理, 这意味着堆栈框架立即回到之前的那个.
- 如果在上层调用者也没找到对应处理器, 该异常会继续被向上移交, 直到找到合适处理器.
- 如果到达最顶层仍然没有找到对应处理器, 那么就认为这个异常是未处理的, Python 解释器会显示出跟踪返回消息, 然后退出.
12.2.3 封装内建函数(示例)
def safe_float(obj):
try:
retval = float(obj)
except ValueError:
retval = 'could not convert non-number to float'
return retval
12.2.4 带有多个 except 的 try 语句
可以把多个 except 语句连接在一起, 处理一个 try 块中可能发生的多种异常,
except Exception1[, reason1]:
suite_for_exception_Exception1
except Exception2[, reason2]:
suite_for_exception_Exception2
:
封装内建函数改善1(示例)
def safe_float(obj):
try:
retval = float(obj)
except ValueError:
retval = 'could not convert non-number to float'
except TypeError:
retval = 'object type cannot be converted to float'
return retval
12.2.5 处理多个异常的 except 语句
我们还可以在一个 except 子句里处理多个异常. except 语句在处理多个异常时要求异常被放在一个元组里:
except (Exception1, Exception2)[, reason]:
suite_for_Exception1_and_Exception2
边的语法展示了如何处理同时处理两个异常. 事实上 except 语句可以处理任意多个异常,前提只是它们被放入一个元组里 , 如下所示:
except (Exc1[, Exc2[, ... ExcN]])[, reason]:
suite_for_exceptions_Exc1_to_ExcN
封装内建函数改善2(示例)
def safe_float(obj):
try:
retval = float(obj)
except (ValueError, TypeError):
retval = 'argument must be a number or numeric string'
return retval
12.3 捕获所有异常
如果我们想要捕获所有的异常呢? 当然可以!
12.3.1 Exception
如果查询异常继承的树结构, 我们会发现 Exception 是在最顶层的, 所以我们的代码可能看起来会是这样:
try:
:
except Exception, e:
# error occurred, log 'e', etc.
另一个我们不太推荐的方法是使用 裸 except 子句:
try:
:
except:
# error occurred, etc.
12.3.2 SystemExit 和KeyboardInterupt
关于捕获所有异常, 你应当知道有些异常不是由于错误条件引起的. 它们是 SystemExit 和KeyboardInterupt .
SystemExit 是由于当前 Python 应用程序需要退出,
KeyboardInterupt 代表用户按下了 CTRL-C (^C) , 想要关闭 Python .
在真正需要的时候, 这些异常却会被异常处理捕获.
一个典型的迂回工作法代码框架可能会是这样:
try:
:
except (KeyboardInterupt, SystemExit):
# user wants to quit
raise
# reraise back to caller
except Exception:
# handle real errors
12.3.3 BaseException
异常被迁移到了 new-style class 上,启用了一个新的"所有异常的母亲", 这个类叫做BaseException
, 异常的继承结构有了少许调整,为了让人们摆脱不得不除创建两个处理器的惯用法.
KeyboardInterrupt 和 SystemExit 被从Exception 里移出, 和Exception 平级:
BaseException
|- KeyboardInterrupt
|- SystemExit
|- Exception
|- (all other current built-in exceptions) 所有当前内建异常
如果你确实需要捕获所有异常, 那么你就得使用新的 BaseException
:
try:
:
except BaseException, e:
# handle all errors
12.3.4 核心风格: 不要处理并忽略所有错误
Python 提供给程序员的 try-except 语句是为了更好地跟踪潜在的错误并在代码里准备好处理异常的逻辑. 这样的机制在其他语言(例如 C ) 是很难实现的. 它的目的是减少程序出错的次数并在出错后仍能保证程序正常执行. 作为一种工具而言, 只有正确得当地使用它, 才能使其发挥作用.
一个不正确的使用方法就是把它作为一个大绷带"绑定"到一大片代码上. 也就是说把一大段程序(如果还不是整个程序源代码的话)放入一个 try 块中, 再用一个通用的 except 语句 "过滤"掉任何致命的错误, 忽略它们.
# this is really bad code
try:
large_block_of_code # bandage of large piece of code
except Exception:
# same as except:
pass
# blind eye ignoring all errors
很明显, 错误无法避免, try-except 的作用是提供一个可以提示错误或处理错误的机制, 而不是一个错误过滤器. 上边这样的结构会忽略许多错误, 这样的用法是缺乏工程实践的表现, 我们不赞同这样做.
底线: 避免把大片的代码装入 try-except 中然后使用 pass 忽略掉错误. 你可以捕获特定的异常并忽略它们, 或是捕获所有异常并采取特定的动作. 不要捕获所有异常,然后忽略掉它们.
12.4 异常参数
12.4.1 “reason”
异常的参数可以在处理器里忽略, 但 Python 提供了保存这个值的语法. 我们已经在上边接触到相关内容: 要想访问提供的异常原因, 你必须保留一个变量来保存这个参数. 把这个参数放在except 语句后, 接在要处理的异常后面. except 语句的这个语法可以被扩展为:
# single exception
except Exception[, reason]:
suite_for_Exception_with_Argument
# multiple exceptions
except (Exception1, Exception2, ..., ExceptionN)[, reason]:
uite_for_Exception1_to_ExceptionN_with_Argument
reason 将会是一个包含来自导致异常的代码的诊断信息的类实例. 异常参数自身会组成一个元组,并存储为类实例(异常类的实例)的属性.上边的第一种用法中,reason将会是一个Exception 类的实例.
对于大多内建异常, 也就是从 StandardError 派生的异常, 这个元组只包含一个指示错误原因的字符串. 一般说来, 异常的名字已经是一个满意的线索了, 但这个错误字符串会提供更多的信息. 操作系统或其他环境类型的错误, 例如 IOError , 元组中会把操作系统的错误编号放在错误字符串前.无论 reason 只包含一个字符串或是由错误编号和字符串组成的元组, 调用 str(reason) 总会返回一个良好可读的错误原因. 不要忘记 reason 是一个类实例 - 这样做你其实是调用类的特殊方法__str__()
.
12.4.2 核心风格: 遵循异常参数规范
当你在自己的代码中引发内建(built-in)的异常时, 尽量遵循规范, 用和已有 Python 代码一致错误信息作为传给异常的参数元组的一部分. 简单地说, 如果你引发一个 ValueError , 那么最好提供和解释器引发 ValueError 时一致的参数信息, 如此类推. 这样可以在保证代码一致性,同时也能避免其他应用程序在使用你的模块时发生错误.
因为每一个异常都将生成自己的异常参数,如果我们选择用这个字符串来而不是我们自定义的 信息,可以提供一个更好的线索来指出问题.下面的代码片段中,我们用字符串化(string representation)的异常参数来替换单一的错误信息.
封装内建函数改善3(示例)
def safe_float(object):
try:
retval = float(object)
except (ValueError, TypeError), diag:
retval = str(diag)
return retval
在此基础上运行我们的新代码,当我们提供 sofe_float()的参数给不恰当时,虽然还是只有一条捕获语句,但是可以获得如下(不同的)信息.
>>> safe_float('xyz')
'invalid literal for float(): xyz'
>>> safe_float({})
'object can't be converted to float'
12.4.3 else 子句
我们已经看过 else 语句段配合其他的 Python 语句,比如条件和循环.至于 try-except 语句段,它的功能和你所见过的其他 else 没有太多的不同,在 try 范围中没有异常被检测到时,执行 else 子句.
在 else 范围中的任何代码运行前,try 范围中的所有代码必须完全成功(也就是,结束前没有引发异常).
def safe_float(object):
try:
retval = float(object)
except (ValueError, TypeError) as reason:
retval = str(reason)
else:
print "everything ok."
return retval
print safe_float(12.23)
everything ok.
12.23
12.4.4 finally 子句
finally 子句是无论异常是否发生,是否捕捉都会执行的一段代码.你可以将 finally 仅仅配合try 一起使用,也可以和 try-except(else 也是可选的)一起使用.
下面是 try-except-else-finally 语法的示例:
try:
A
except MyException: B
else: C
finally: D
可能的顺序是A-C-D[正常]或 A-B-D[异常]).无论异常发生在 A,B,和/或 C 都将执行 finally 块D.
12.4.4.1 try-finally
另一种使用 finally 的方式是 finally 单独和 try 连用.这个 try-finally 语句和 try-except区别在于它不是用来捕捉异常的.作为替代,它常常用来维持一致的行为而无论异常是否发生.我们得知无论 try 中是否有异常触发,finally 代码段都会被执行
try:
try_suite
finally:
finally_suite
#无论如何都执行
当在 try 范围中产生一个异常时,(这里)会立即跳转到 finally 语句段.当 finally 中的所有代 码都执行完毕后,会继续向上一层引发异常.
有一种好的方式来关闭文件而无论错误是否发生?我们可以通过 try-finally 来实现:
ccfile = None
try:
try:
ccfile = open('carddata.txt', 'r')
txns = ccfile.readlines()
except IOError:
log.write('no txns this month\n')
finally:
if ccfile:
ccfile.close()
代码片段会尝试打开文件并且读取数据.如果在其中的某步发生一个错误,会写入日志,随后文件被正确的关闭.如果没有错误发生,文件也会被关闭.(同样的功能可以通过上面标准化的try-except-finally 语句段实现).另一种可选的实现切换了 try-except 和try-finally 包含的方式,如:
ccfile = None
try:
try:
ccfile = open('carddata.txt', 'r')
txns = ccfile.readlines()
finally:
if ccfile:
ccfile.close()
except IOError:
log.write('no txns this month\n')
一个最终的注意点:如果 finally 中的代码引发了另一个异常或由于 return,break,continue 语法而终止,原来的异常将丢失而且无法重新引发.
12.4.4.2 try-except-else-finally:厨房一锅端
我们综合了这一章目前我们所见过的所有不同的可以处理异常的语法样式:
try:
try_suite
except Exception1:
suite_for_Exception1
except (Exception2, Exception3, Exception4):
suite_for_Exceptions_2_3_and_4
except Exception5, Argument5:
suite_for_Exception5_plus_argument
except (Exception6, Exception7), Argument67:
suite_for_Exceptions6_and_7_plus_argument
except:
suite_for_all_other_exceptions
else:
no_exceptions_detected_suite
finally:
always_execute_suite
这一节重要的是无论你选择什么语法,你至少要有一个 except 子句,而 else 和 finally 都是可选的.