深入理解Python异常处理的哲学


Posted in Python onFebruary 01, 2019

所谓异常指的是程序的执行出现了非预期行为,就好比现实中的做一件事过程中总会出现一些意外的事。异常的处理是跨越编程语言的,和具体的编程细节相比,程序执行异常的处理更像是哲学。限于认知能力和经验所限,不可能达到像解释器下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

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
/ 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'

可以看到纵使程序发生异常且没有被正确处理,在程序终止前,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:
/ 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:
/ 0
except Exception as e:
 trace_mgr.handle_one_exception()
 # process trace
/ '0'

结果用两种方式收集到两个trace信息:

is_hook: False
Traceback (most recent call last):
File "D:\My Folders\Cnblogs\Alpha Panda\Main.py", line 299, in <module>
/ 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>
/ '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。

异常的陷阱:

正确的异常处理能让代码有更好的鲁棒性,但是错误的使用异常会过犹不及。

捕获异常却忽略掉或者错误的处理是不可取的。滥用异常处理不仅达不到提高系统稳定性的效果,还会隐藏掉引起错误的诱因,导致排查问题的难度增加。

因此比如何捕获异常更重要的是,异常发生时应当如何处理。

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Python 相关文章推荐
详解Python中的条件判断语句
May 14 Python
Python3实现Web网页图片下载
Jan 28 Python
详解Python迭代和迭代器
Mar 28 Python
详解Python之数据序列化(json、pickle、shelve)
Mar 30 Python
python决策树之CART分类回归树详解
Dec 20 Python
Python实现简单文本字符串处理的方法
Jan 22 Python
Django添加feeds功能的示例
Aug 07 Python
windows7 32、64位下python爬虫框架scrapy环境的搭建方法
Nov 29 Python
python自动化实现登录获取图片验证码功能
Nov 20 Python
python 实现在无序数组中找到中位数方法
Mar 03 Python
Python自动创建Excel并获取内容
Sep 16 Python
Python LMDB库的使用示例
Feb 14 Python
python for 循环获取index索引的方法
Feb 01 #Python
对python For 循环的三种遍历方式解析
Feb 01 #Python
python使用for循环计算0-100的整数的和方法
Feb 01 #Python
python 用for循环实现1~n求和的实例
Feb 01 #Python
python计算阶乘和的方法(1!+2!+3!+...+n!)
Feb 01 #Python
python 阶乘累加和的实例
Feb 01 #Python
在Python 中同一个类两个函数间变量的调用方法
Jan 31 #Python
You might like
PHP OPCode缓存 APC详细介绍
2010/10/12 PHP
基于php socket(fsockopen)的应用实例分析
2013/06/02 PHP
PHP的foreach中使用引用时需要注意的一个问题和解决方法
2014/05/29 PHP
PHP中SSO Cookie登录分析和实现
2015/11/06 PHP
PHP编程入门的基本语法知识点总结
2016/01/26 PHP
PHP数组操作简单案例分析
2016/10/15 PHP
PHP后台备份MySQL数据库的源码实例
2019/03/18 PHP
PHP7生产环境队列Beanstalkd用法详解
2020/05/19 PHP
javascript将数组插入到另一个数组中的代码
2013/01/10 Javascript
Jquery和JS用外部变量获取Ajax返回的参数值的方法实例(超简单)
2013/06/17 Javascript
javascript中CheckBox全选终极方案
2015/05/20 Javascript
JavaScript中的条件判断语句使用详解
2015/06/03 Javascript
纯css下拉菜单 无需js
2016/08/15 Javascript
关于js原型的面试题讲解
2016/09/25 Javascript
详解js中==与===的区别
2017/01/08 Javascript
WebView启动支付宝客户端支付失败的问题小结
2017/01/11 Javascript
p5.js入门教程之鼠标交互的示例
2018/03/16 Javascript
vue2.0$nextTick监听数据渲染完成之后的回调函数方法
2018/09/11 Javascript
微信小程序之数据绑定原理解析
2019/08/14 Javascript
解决LayUI加上form.render()下拉框和单选以及复选框不出来的问题
2019/09/27 Javascript
基于JS实现视频上传显示进度条
2020/05/12 Javascript
基于Echarts图表在div动态切换时不显示的解决方式
2020/07/20 Javascript
Python实现压缩和解压缩ZIP文件的方法分析
2017/09/28 Python
python实战教程之自动扫雷
2018/07/13 Python
Python HTML解析器BeautifulSoup用法实例详解【爬虫解析器】
2019/04/05 Python
查看Python依赖包及其版本号信息的方法
2019/08/13 Python
Python Selenium安装及环境配置的实现
2020/03/17 Python
Html5游戏开发之乒乓Ping Pong游戏示例(二)
2013/01/21 HTML / CSS
怎样声明一个匿名的内部类
2016/06/01 面试题
4s店机修工岗位职责
2013/12/20 职场文书
互联网电子商务专业毕业生求职信
2014/03/18 职场文书
人事任命书怎么写
2014/06/05 职场文书
2014年建筑工作总结
2014/11/26 职场文书
保护环境建议书作文300字
2015/09/14 职场文书
2016年庆祝六一儿童节活动总结
2016/04/06 职场文书
Vue如何清空对象
2022/03/03 Vue.js