所谓异常指的是程序的执行出现了非预期行为, 就好比现实中的做一件事过程中总会出现一些意外的事. 异常的处理是跨越编程语言的, 和具体的编程细节相比, 程序执行异常的处理更像是哲学. 限于认知能力和经验所限, 不可能达到像解释器下 import this 看到的 python 设计之禅一样, 本文就结合实际使用简单的聊一聊.
0. 前言
工作中, 程序员之间一言不合就亮代码, 毕竟不管是代码本身还是其执行过程, 不会存在二义性, 更不会含糊不清, 代码可谓是程序员之间的官方语言. 但是其处理问题的逻辑或者算法则并非如此.
让我至今记忆犹新的两次程序员论剑有:
反问一: 项目后期所有的异常处理都要去掉, 不允许上线后出现未知的异常, 把你这里的异常处理去掉, 换成 if else;
反问二: 这里为什么要进行异常处理? 代码都是你写的, 怎么会出现异常呢?
这是我亲身经历的, 不知道大家碰到这两个问题会怎样回答, 至少我当时竟无言以对. 这两个问题分别在不同的时间针对不同的问题出自一个互联网巨头中某个资深 QA 和资深开发的反问.
暂且不论对错, 毕竟不同人考虑问题的出发点是不同的. 但是从这么坚决的去异常处理的回答中至少有一点可以肯定, 那就是很多人对自己的代码太过自信或者说是察觉代码潜在问题的直觉力不够, 更别提正确的处理潜在的问题以保证重要业务逻辑的处理流程. 写代码的时候如果只简单考虑正常的情况, 那是在往代码中下毒.
接下类本篇博文将按照套路出牌(避免被 Ctrl + W), 介绍一下 python 的异常处理的概念和具体操作.
1. 为什么要异常处理
常见的程序 bug 无非就两大类:
语法错误;
逻辑不严谨或者思维混乱导致的逻辑错误;
显然第二种错误更难被发现, 且后果往往更严重. 无论哪一种 bug, 有两种后果等着我们: 一, 程序崩掉; 二, 执行结果不符合预期;
对于一些重要关键的执行操作, 异常处理可以控制程序在可控的范围执行, 当然前提是正确的处理.
比如我们给第三方提供的 API 或者使用第三方提供的 API. 多数情况下要正确的处理调用者错误的调用参数和返回异常结果的情况, 不然就可能要背黑锅了.
在不可控的环境中运行程序, 异常处理是必须的. 然而困难的地方是当异常发生时, 如何进行处理.
2. python 异常处理
下面逐步介绍一下 python 异常处理相关的概念.
2.1 异常处理结构
必要的结构为 try ... except, 至少有一个 except,else 和 finally 可选.
- try:
- code blocks
- except (Exception Class1, Exception Class2, ...) as e:
- catch and process exception
- except Exception ClassN:
- catch and process exception
- ... ...
- else:
- when nothing unexpected happened
- finally:
- always executed when all to end
2.2 python 内置异常类型
模块 exceptions 中包含了所有内置异常类型, 类型的继承关系如下:
- BaseException
- +-- SystemExit
- +-- KeyboardInterrupt
- +-- GeneratorExit
- +-- Exception
- +-- StopIteration
- +-- StandardError
- | +-- BufferError
- | +-- ArithmeticError
- | | +-- FloatingPointError
- | | +-- OverflowError
- | | +-- ZeroDivisionError
- | +-- AssertionError
- | +-- AttributeError
- | +-- EnvironmentError
- | | +-- IOError
- | | +-- OSError
- | | +-- WindowsError (Windows)
- | | +-- VMSError (VMS)
- | +-- EOFError
- | +-- ImportError
- | +-- LookupError
- | | +-- IndexError
- | | +-- KeyError
- | +-- MemoryError
- | +-- NameError
- | | +-- UnboundLocalError
- | +-- ReferenceError
- | +-- RuntimeError
- | | +-- NotImplementedError
- | +-- SyntaxError
- | | +-- IndentationError
- | | +-- TabError
- | +-- SystemError
- | +-- TypeError
- | +-- ValueError
- | +-- UnicodeError
- | +-- UnicodeDecodeError
- | +-- UnicodeEncodeError
- | +-- UnicodeTranslateError
- +-- Warning
- +-- DeprecationWarning
- +-- PendingDeprecationWarning
- +-- RuntimeWarning
- +-- SyntaxWarning
- +-- UserWarning
- +-- FutureWarning
- +-- ImportWarning
- +-- UnicodeWarning
- +-- BytesWarning
- View Code
- 2.3 except clause
excpet 子句的常用的写法如下:
- except: # 默认捕获所有类型的异常
- except Exception Class: # 捕获 Exception Class 类型的异常
- except Exception Class as e: # 捕获 Exception Class 类型的异常, 异常对象赋值到 e
- except (Exception Class1, Exception Class2, ...) as e: # 捕获列表中任意一种异常类型
上面的异常类可以是下面 python 内置异常类型, 也可以是自定义的异常类型.
2.4 异常匹配原则
所有 except 子句按顺序一一匹配, 匹配成功则忽略后续的 except 子句;
若抛出异常对象为 except 子句中给出的异常类型的对象或给出的异常类型的派生类对象, 则匹配成功;
如果所有的 except 子句均匹配失败, 异常会向上传递;
如果依然没有被任何 try...except 捕获到, 程序在终止前会调用 sys.excepthook 进行处理;
2.5 else & finally
如果没有异常发生, 且存在 else 子句, 则执行 else 子句. 只要存在 finally 子句, 无论任何情况下都会被执行.
可能唯一不好理解的地方就是 finally. 没有异常, 捕获异常, 异常上传以及异常处理过程中发生异常等均会执行 finally 语句.
下面看个例子:
- def division(a, b):
- try:
- print'res = %s' % (a / b)
- except (ZeroDivisionError, ArithmeticError) as e:
- return str(e) # 注意此处使用的是 return
- else:
- print '%s / %s = %s' % (a, b, a / b)
- finally:
- print 'finally clause'
分别输入参数 (1, 2),(1, 0) 和 (1,"0")执行:
print 'return value: %s' % division(a, b)
得到的结果如下:
- res = 0
- 1 / 2 = 0
- finally clause
- return value: None
- finally clause
- return value: integer division or modulo by zero
- finally clause
- Traceback (most recent call last):
- File "D:\My Folders\Cnblogs\Alpha Panda\Main.py", line 217, in <module>
- print 'return value: %s' % division(1, "0")
- File "D:\My Folders\Cnblogs\Alpha Panda\Main.py", line 208, in division
- print'res = %s' % (a / b)
- TypeError: unsupported operand type(s) for /: 'int' and 'str'
- View Code
可以看到纵使程序发生异常且没有被正确处理, 在程序终止前, finally 语句依旧被执行了. 可以将此看做程序安全的最后一道有效屏障. 主要进行一些善后清理工作, 比如资源释放, 断开网络连接等. 当然 with 声明可以自动帮我们进行一些清理工作.
2.6 raise 抛出异常
程序执行过程中可以使用 raise 主动的抛出异常.
- try:
- e = Exception('Hello', 'World')
- e.message = 'Ni Hao!'
- raise e
- except Exception as inst:
- print type(inst), inst, inst.args, inst.message
结果:<type 'exceptions.Exception'> ('Hello', 'World') ('Hello', 'World') Ni Hao!
上面展示了 except 对象的属性 args, message.
2.7 自定义异常
绝大部分情况下内置类型的异常已经能够满足平时的开发使用, 如果想要自定义异常类型, 可以直接继承内置类型来实现.
- class ZeroDivZeroError(ZeroDivisionError):
- def __init__(self, value):
- self.value = value
- def __str__(self):
- return repr(self)
- def __repr__(self):
- return self.value
- try:
- # do something and find 0 / 0
- raise ZeroDivZeroError('hahajun')
- except ZeroDivZeroError as err:
- print 'except info %s' % err
自定义异常应该直接继承自 Exception 类或其子类, 而不要继承自 BaseException.
3. Stack Trace
python 执行过程中发生异常, 会告诉我们到底哪里出现问题和什么问题. 这两种类型的错误信息分别为 stack trace 和 exception, 在程序中分别用 traceback object 和异常对象表示.
- Traceback (most recent call last):
- File "D:\My Folders\Cnblogs\Alpha Panda\Main.py", line 270, in <module>
- 1 / 0
- ZeroDivisionError: integer division or modulo by zero
上面的错误信息包含错误发生时当前的堆栈信息 (stack trace, 前三行) 和异常信息(exception, 最后一行), 分别存放在 traceback objects 和抛出的异常对象中.
异常对象及异常信息前面已经介绍过, 接下来我们在看一下异常发生时, stack trace 的处理.
Traceback objects represent a stack trace of an exception. A traceback object is created when an exception occurs.
这时有两种情况:
异常被 try...except 捕获
没有被捕获或者干脆没有处理
正常的代码执行过程, 可以使用 traceback.print_stack()输出当前调用过程的堆栈信息.
3.1 捕获异常
对于第一种情况可以使用下面两种方式获取 stack trace 信息:
trace_str = traceback.format_exc()
或者从 sys.exc_info()中获取捕获的异常对象等的信息, 然后格式化成 trace 信息.
- def get_trace_str(self):
- """
- 从当前栈帧或者之前的栈帧中获取被 except 捕获的异常信息;
- 没有被 try except 捕获的异常会直接传递给 sys.excepthook
- """
- t, v, tb = sys.exc_info()
- trace_info_list = traceback.format_exception(t, v, tb)
- trace_str = ' '.join(trace_info_list)
至于抛出的包含异常信息的异常对象则可以在 try...except 结构中的 except Exception class as e 中获取.
3.2 未捕获异常
第二种情况, 如果异常没有被处理或者未被捕获则会在程序推出前调用 sys.excepthook 将 traceback 和异常信息输出到 sys.stderr.
- def except_hook_func(tp, val, tb):
- trace_info_list = traceback.format_exception(tp, val, tb)
- trace_str = ' '.join(trace_info_list)
- print 'sys.excepthook'
- print trace_str
- sys.excepthook = except_hook_func
上面自定义 except hook 函数来取代 sys.excepthook 函数. 在 hook 函数中根据异常类型 tp, 异常值和 traceback 对象 tb 获取 stack trace. 这种情况下不能从 sys.exc_info 中获取异常信息.
3.3 测试
- def except_hook_func(tp, val, tb):
- trace_info_list = traceback.format_exception(tp, val, tb)
- trace_str = ' '.join(trace_info_list)
- print 'sys.excepthook'
- print trace_str
- sys.excepthook = except_hook_func
- try:
- 1 / 0
- except TypeError as e:
- res = traceback.format_exc()
- print "try...except"
- print str(e.message)
- print res
走的是 sys.excepthook 处理流程结果:
- sys.excepthook
- Traceback (most recent call last):
- File "D:\My Folders\Cnblogs\Alpha Panda\Main.py", line 259, in <module>
- 1 / 0
- ZeroDivisionError: integer division or modulo by zero
将 except TypeError as e 改为 except ZeroDivisionError as e, 则走的是 try...except 捕获异常流程, 结果如下:
- try...except
- integer division or modulo by zero
- Traceback (most recent call last):
- File "D:\My Folders\Cnblogs\Alpha Panda\Main.py", line 259, in <module>
- 1 / 0
- ZeroDivisionError: integer division or modulo by zero
4. 异常信息收集
讲了这么多, 我们看一下如何实现一个程序中 trace 信息的收集.
- class TracebackMgr(object):
- def _get_format_trace_str(self, t, v, tb):
- _trace = traceback.format_exception(t, v, tb)
- return ' '.join(_trace)
- def handle_one_exception(self):
- """
- 从当前栈帧或者之前的栈帧中获取被 except 捕获的异常信息;
- 没有被 try except 捕获的异常会自动使用 handle_traceback 进行收集
- """
- t, v, tb = sys.exc_info()
- self.handle_traceback(t, v, tb, False)
- def handle_traceback(self, t, v, tb, is_hook = True):
- """
- 将此函数替换 sys.excepthook 以能够自动收集没有被 try...except 捕获的异常,
- 使用 try except 处理的异常需要手动调用上面的函数 handle_one_exception 才能够收集
- """
- trace_str = self._get_format_trace_str(t, v, tb)
- self.record_trace(trace_str, is_hook)
- # do something else
- def record_trace(self, trace_str, is_hook):
- # Do somethind
- print 'is_hook: %s' % is_hook
- print trace_str
其用法很简单:
- trace_mgr = TracebackMgr()
- sys.excepthook = trace_mgr.handle_traceback
- try:
- 1 / 0
- except Exception as e:
- trace_mgr.handle_one_exception()
- # process trace
- 1 / '0'
结果用两种方式收集到两个 trace 信息:
- is_hook: False
- Traceback (most recent call last):
- File "D:\My Folders\Cnblogs\Alpha Panda\Main.py", line 299, in <module>
- 1 / 0
- ZeroDivisionError: integer division or modulo by zero
- is_hook: True
- Traceback (most recent call last):
- File "D:\My Folders\Cnblogs\Alpha Panda\Main.py", line 304, in <module>
- 1 / '0'
- TypeError: unsupported operand type(s) for /: 'int' and 'str'
可以将标准的输入和输出重定向, 将打印日志和错误信息输入到文件中:
- class Dumpfile(object):
- @staticmethod
- def write(str_info):
- with open('./dump_file.txt', 'a+') as fobj:
- fobj.write(str_info)
- def flush(self):
- self.write('')
- sys.stdout = sys.stderr = Dumpfile()
trace 的收集主要用到两点: 如何捕获异常和两种情况下异常信息的收集, 前面都介绍过.
5. 总结
python 异常处理:
使用对象来表示异常错误信息, 每种异常均有一种对应的类, BaseException 为所有表示异常处理类的基类.
程序执行过程中抛出的异常会匹配该对象对应的异常类和其所有的基类.
可以从内置类型的异常类派生出自定义的异常类.
被捕获的异常可以再次被抛出.
可以的话尽量使用内置的替代方案, 如 if getattr(obj, attr_name, None), 或者 with 结构等.
sys.exc_info()保存当前栈帧或者之前的栈帧中获取被 try, except 捕获的异常信息.
未处理的异常导致程序终止前会被 sys.excpethook 处理, 可以自定义定义 sys.excpethook.
异常的陷阱:
正确的异常处理能让代码有更好的鲁棒性, 但是错误的使用异常会过犹不及.
捕获异常却忽略掉或者错误的处理是不可取的. 滥用异常处理不仅达不到提高系统稳定性的效果, 还会隐藏掉引起错误的诱因, 导致排查问题的难度增加.
因此比如何捕获异常更重要的是, 异常发生时应当如何处理.
来源: https://www.cnblogs.com/yssjun/p/10326300.html