深入理解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爬虫之神器正则表达式
Nov 06 Python
Python2.x利用commands模块执行Linux shell命令
Mar 11 Python
Python 性能优化技巧总结
Nov 01 Python
Django如何实现内容缓存示例详解
Sep 24 Python
Python使用pydub库对mp3与wav格式进行互转的方法
Jan 10 Python
tensorflow没有output结点,存储成pb文件的例子
Jan 04 Python
PyQt5中多线程模块QThread使用方法的实现
Jan 31 Python
python中setuptools的作用是什么
Jun 19 Python
python文件读取失败怎么处理
Jun 23 Python
使用jupyter notebook运行python和R的步骤
Aug 13 Python
python将下载到本地m3u8视频合成MP4的代码详解
Nov 24 Python
Python爬虫之Selenium鼠标事件的实现
Dec 04 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使用array_rand()函数从数组中随机选择一个或多个元素
2014/04/28 PHP
PHP基于单例模式实现的mysql类
2016/01/09 PHP
PHP基于迭代实现文件夹复制、删除、查看大小等操作的方法
2017/08/11 PHP
PHP替换Word中变量并导出PDF图片的实现方法
2020/11/26 PHP
PHP7 整型处理机制修改
2021/03/09 PHP
Jquery 绑定时间实现代码
2011/05/03 Javascript
JS与C#编码解码
2013/12/03 Javascript
js中call与apply的用法小结
2013/12/28 Javascript
JavaScript charCodeAt方法入门实例(用于取得指定位置字符的Unicode编码)
2014/10/17 Javascript
node.js中的dns.getServers方法使用说明
2014/12/08 Javascript
Ajax 加载数据 练习代码
2017/01/05 Javascript
jQuery使用DataTable实现删除数据后重新加载功能
2017/02/27 Javascript
JavaScript 数据类型详解
2017/03/13 Javascript
JS简单获取当前日期时间的方法(如:2017-03-29 11:41:10 星期四)
2017/03/29 Javascript
详解Vue.js基于$.ajax获取数据并与组件的data绑定
2017/05/26 Javascript
从parcel.js打包出错到选择nvm的全部过程
2018/01/23 Javascript
vue-cli中的babel配置文件.babelrc实例详解
2018/02/22 Javascript
JavaScript设计模式之观察者模式实例详解
2019/01/16 Javascript
js取0-9随机取4个数不重复的数字代码实例
2019/03/27 Javascript
最简单的vue消息提示全局组件的方法
2019/06/16 Javascript
vue 子组件watch监听不到prop的解决
2020/08/09 Javascript
python导出chrome书签到markdown文件的实例代码
2017/12/27 Python
python 定义给定初值或长度的list方法
2018/06/23 Python
python绘制圆柱体的方法
2018/07/02 Python
超实用的 30 段 Python 案例
2019/10/10 Python
Sofft鞋官网:世界知名鞋类品牌
2017/03/28 全球购物
家乐福台湾线上购物网:Carrefour台湾
2020/09/15 全球购物
怎样声明一个匿名的内部类
2016/06/01 面试题
计算机操作自荐信
2013/12/07 职场文书
《胡杨》教学反思
2014/02/16 职场文书
试用期自我评价怎么写
2015/03/10 职场文书
2015年实习班主任工作总结
2015/04/23 职场文书
追悼会答谢词范文
2015/09/29 职场文书
纯CSS实现酷炫的霓虹灯效果
2021/04/13 HTML / CSS
MySQL索引失效场景及解决方案
2022/07/23 MySQL
CSS list-style-type属性使用方法
2023/05/21 HTML / CSS