深入理解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+selenium识别验证码并登录的示例代码
Dec 21 Python
浅谈pandas筛选出表中满足另一个表所有条件的数据方法
Feb 08 Python
详解Django中CBV(Class Base Views)模型源码分析
Feb 25 Python
python查询文件夹下excel的sheet名代码实例
Apr 02 Python
Django如何自定义model创建数据库索引的顺序
Jun 20 Python
Python中的pathlib.Path为什么不继承str详解
Jun 23 Python
对DJango视图(views)和模版(templates)的使用详解
Jul 17 Python
Python爬虫:将headers请求头字符串转为字典的方法
Aug 21 Python
在pycharm中使用matplotlib.pyplot 绘图时报错的解决
Jun 01 Python
celery在python爬虫中定时操作实例讲解
Nov 27 Python
Python下opencv库的安装过程及问题汇总
Jun 11 Python
Pandas数据类型之category的用法
Jun 28 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
xajax写的留言本
2006/11/25 PHP
PHP 缓存实现代码及详细注释
2010/05/16 PHP
php中使用$_REQUEST需要注意的一个问题
2013/05/02 PHP
php 模拟GMAIL,HOTMAIL(MSN),YAHOO,163,126邮箱登录的详细介绍
2013/06/18 PHP
Smarty实现页面静态化(生成HTML)的方法
2016/05/23 PHP
Yii2中使用join、joinwith多表关联查询
2016/06/30 PHP
phpStudy配置多站点多域名方法及遇到的403错误解决方法
2017/10/19 PHP
拖动Html元素集合 Drag and Drop any item
2006/12/22 Javascript
利用WebBrowser彻底解决Web打印问题(包括后台打印)
2009/06/22 Javascript
有趣的javascript数组定义方法
2010/09/10 Javascript
动态的改变IFrame的高度实现IFrame自动伸展适应高度
2012/12/28 Javascript
Ext JS框架中日期函数的用法及日期选择控件的实现
2016/05/21 Javascript
一个简单的JavaScript Map实例(分享)
2016/08/03 Javascript
JavaScript Canvas绘制圆形时钟效果
2020/08/20 Javascript
Vue修改mint-ui默认样式的方法
2018/02/03 Javascript
记一次Vue.js混入mixin的使用(分权限管理页面)
2019/04/17 Javascript
微信小程序自定义单项选择器样式
2019/07/25 Javascript
layui实现tab的添加拒绝重复的方法
2019/09/04 Javascript
JS原型和原型链原理与用法实例详解
2020/02/05 Javascript
vue 图片裁剪上传组件的实现
2020/11/12 Javascript
Python socket C/S结构的聊天室应用实现
2014/11/30 Python
pandas如何处理缺失值
2019/07/31 Python
python对Excel按条件进行内容补充(推荐)
2019/11/24 Python
python 安装教程之Pycharm安装及配置字体主题,换行,自动更新
2020/03/13 Python
python一些性能分析的技巧
2020/08/30 Python
Python爬取豆瓣数据实现过程解析
2020/10/27 Python
python中reload重载实例用法
2020/12/15 Python
2014新年寄语
2014/01/20 职场文书
市场安全管理制度
2014/01/26 职场文书
药店促销活动策划方案
2014/08/24 职场文书
白酒代理协议书范本
2014/10/26 职场文书
2015新生加入学生会自荐书
2015/03/24 职场文书
西安事变观后感
2015/06/12 职场文书
团支部书记竞选稿
2015/11/21 职场文书
导游词之云南丽江古城
2019/09/17 职场文书
Redis 操作多个数据库的配置的方法实现
2022/03/23 Redis