异常处理
在 Python 中,“异常”是指在程序运行期间出现的错误,通常语法是正确的,但是运行时出错,比如除零操作、使用没有初始化的变量等等。而运行前发生的则称为“错误”,比如语法错误。
如果异常对象未被处理或捕获,程序就会出现回溯(traceback)并终止执行。所以要想写出高可靠性的 Python 代码,必须在可能出现异常的地方捕获并处理异常。Python 提供了 try…except 来捕获异常,举个例子:
def div(dividend, divisor):
try:
quotient = dividend/divisor
return quotient
except ZeroDivisionError:
print("除数不能为零啊,哥!")
print("div(4,2) => %d" %(div(4,2)))
print("div(4,0) => %d" %(div(4,0)))
执行结果如下:
div(4,2) => 2
除数不能为零啊,哥!
Traceback (most recent call last):
File "except_error.py", line 9, in <module>
print("div(4,0) => %d" %(div(4,0))) TypeError: %d format: a number is required, not NoneType
在这个例子中,函数 div() 中的除零异常已经被捕获了,后面的 traceback 其实是格式化输出的异常,以为当除数为零时 div() 函数没有返回一个数字,所以 div(4,0) 得到的结果是 None。
Python 中异常处理的结构如下:
try:
# 尝试执行的语句
except [ErrorType [as err] ]:
# 捕获异常时执行的语句
else:
# 没有异常时执行的语句
finally:
# 无论如何最后都要执行的语句
在这个结构中,try 子句是必须的,except 子句、else 子句和 finally 子句都是可选的。也就是说,可以是 try-except
结构,或者 try-except-else
结构,或者 try-finally
结构,或者是完整的 try-except-else-finally
结构。
值得一提的是,异常的类型很多,所以 except 子句可以是一条或者多条;另外,当存在 except 子句时,else 子句才有意义,否则就相当于所有所有执行内容写在 try 子句中了。
try 语句的工作方式如下:
- 首先,执行 try 子句(在关键字 try 和关键字 except 之间的语句);如果没有异常发生,忽略 except 子句,接着执行有 else 子句(如果有);最后执行 finally 子句;
- 如果在执行 try 子句的过程中发生异常,那么 try 子句余下部分都会被忽略,转而执行 except 子句;
- 如果异常的类型和 except 后面的名称相符,那么对应 except 子句将被执行;
- try 语句可以嵌套使用,如果一个异常没有任何的 except 匹配,那么这个异常将会传递给上层的 try 中;
- 一个 try 语句可能包含多个 except 子句,分别来处理不同的(特定的)异常,最多只有一个分支会被执行;
- 一个 except 子句可以同时处理多个异常,这些异常将被放在一个括号里成为一个元组。
另外,还需要注意以下特性:
- 无论 try 里执行什么,即使是 return,也会调用 finally 的(无论是否有异常,最后都要做的一些事情);
- try 子句的返回值会被 finally 的返回值覆盖;
- 如果 try 中没有异常,那么 except 部分将跳过,执行 else 中的语句;
- 但是如果 try 里面执行了 return 语句,就会跳过 else 子句,从而阻挡了 else 的执行,但是不会影响 finally 的执行;
因此,我们编写 Python 代码过程中应注意:如非必须,不要在 try 和 else 子句中写 return 语句,return 语句应该写在 finally 中(如果没有 finally,就写在最后)。
简单来说,就是 try、except、else 里都是 do something 的,Return 的工作就交给 finally 吧。
异常类型
在 Python 中,我们通过 dir(__builtins__)
可以看到内建的一些异常类型,各种异常及其说明如下:
异常名称 | 描述 |
---|---|
BaseException | 所有异常的基类 |
SystemExit | 解释器请求退出 |
KeyboardInterrupt | 用户中断执行(通常是输入^C) |
Exception | 常规错误的基类 |
StopIteration | 迭代器没有更多的值 |
GeneratorExit | 生成器(generator)发生异常来通知退出 |
StandardError | 所有的内建标准异常的基类 |
ArithmeticError | 所有数值计算错误的基类 |
FloatingPointError | 浮点计算错误 |
OverflowError | 数值运算超出最大限制 |
ZeroDivisionError | 除(或取模)零 (所有数据类型) |
AssertionError | 断言语句失败 |
AttributeError | 对象没有这个属性 |
EOFError | 没有内建输入,到达EOF 标记 |
EnvironmentError | 操作系统错误的基类 |
IOError | 输入/输出操作失败 |
OSError | 操作系统错误 |
WindowsError | 系统调用失败 |
ImportError | 导入模块/对象失败 |
LookupError | 无效数据查询的基类 |
IndexError | 序列中没有此索引(index) |
KeyError | 映射中没有这个键 |
MemoryError | 内存溢出错误(对于Python 解释器不是致命的) |
NameError | 未声明/初始化对象 (没有属性) |
UnboundLocalError | 访问未初始化的本地变量 |
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 | 用户代码生成的警告 |
自定义异常
除了 Python 定义的异常之外,我们还可以自定义异常。自定义异常实际上就是定义一个类,让这个类继承 Exception 类(或其派生类)。但通常我们并不会使用自定义异常,所以了解即可。
举个栗子:
class NetworkError(RuntimeError):
def __init__(self, arg):
self.arg = arg
def connect():
raise NetworkError("Bad hostname")
try:
connect()
except NetworkError as err:
print(err)
执行后输出:
Bad hostname
抛出异常
除了捕获异常,我们在 Python 中还可抛出异常,或者称为触发异常。raise 语句的格式如下:
raise [Exception [, args [, traceback]]]
语句中 Exception 是异常的类型(例如 NameError)参数标准异常中任一种,args 是自已提供的异常参数。最后一个 traceback 参数是可选的(很少使用),如果存在,是跟踪异常对象。
举个栗子:
def func(num):
if num < 1:
raise Exception("Invalid level!", num)
try:
func(0)
except Exception as err:
print("No: %s" %(err))
执行后输出:
No: ('Invalid level!', 0)
Python 中的断言
除了使用 try 语句来处理程序运行中出现的异常,Python 为我们提供的断言(Assertions)也可以处理异常。
断言使用 assert 关键字,它的语法格式为:
assert Expression[, Arguments]
当它遇到一个断言语句,Python 解释器评估计算之后的表达式,希望是 true 值。如果表达式为 false,就会触发 AssertionError 异常。
assert 的异常参数(Arguments),其实就是在断言表达式后添加字符串信息,用来解释断言并更好的知道是哪里出了问题。
理解断言的最简单的方法就是把它比作 raise-if 语句(更确切来说是 raise-if-not)。即对一个表达式进行测试,如果结果为 false,则触发异常。
也就是说,assert expression
语句等价于:
if not expression:
raise AssertionError
如果断言失败,Python 使用 Arguments 作为 AssertionError 异常的参数。同样可以用 try-except 语句来捕获并处理 AssertionError 异常,但是如果没有处理 AssertionError,Python 将终止该程序并产生一个回溯。
assert 判断的是表达式,如果需要同时判断多个表达式,可以这样:
assert (expression1 and expression2), "prompt..."
实际上,断言是一种理智检查,通常用于开发阶段,在需要确保某个条件一定为真才能让程序正常工作的情况就很适合使用断言。比如,程序员常常放置断言来检查输入的有效,或在一个函数调用后检查有效的输出。当程序的测试完成,可以选择将断言打开或关闭。
关闭断言的方法是在执行 Python 程序的时候加上 -O 参数,如下:
python -O 源文件
assert 语句不应用于必须执行以确保程序正确的代码,因为如果 Python 运行在最优模式中(通过对解释器使用 -O 选项进入该模式),assert 语句将会被忽略。特别是不能用 assert 语句检查用户输入,相反,assert 语句用于检查应该始终为真的内容。如果 assert 引发异常,这意味着程序中存在 bug,而不是用户出现了错误。因此,对于诸如检查输入是否有效的操作,应该使用传统的 if 语句和相应的错误处理代码替换 assert 语句。
除了 assert 语句之外,Python 还提供内置的只读变量 __debug__, 除非解释器运行在最优模式中(通过使用 -O 选项指定),否则它的值为 True。在运行额外的错误检查过程时,程序可能在需要时检查这个变量。
__debug__ 变量的底层实现在解释器中经过了优化,因为实际上没有包含 if 语句本身的额外控制流逻辑。如果 Python 运行在普通模式中,if __debug__
语句下的语句就会内联到没有 if 语句本身的程序中。而在最优模式中,if __debug__
语句以及所有相关语句都将从程序中完全删除。
使用 assert 和 __debug__ 语句可以对程序进行高效的双模式开发。例如在调试模式中,可以随意地在代码中加入断言和调试检查,以便验证操作正确与否。在最优模式中,将省略所有这些额外的检查,以免造成额外的性能负担。
【参考】