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

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

异常的陷阱:

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

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

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

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

(0)

相关推荐

  • Python异常处理例题整理

    什么是异常? 异常是Python对象,表示一个错误.当Python脚本发生异常时我们需要捕获处理它,否则程序会终止执行.在程序运行过程中,总会遇到各种各样的错误,有的错误是程序编写有问题造成的 还有一类错误是完全无法在程序运行过程中预测的:一切异常皆是对象 系统定义的异常: BaseException 所有异常的基类,父类 Exception 常规错误的基类 StandardError 所有的内建标准异常的基类,标准化错误 ImportError  导入模块错误 ArithmeticError

  • Python学习笔记之读取文件、OS模块、异常处理、with as语法示例

    本文实例讲述了Python学习笔记之读取文件.OS模块.异常处理.with as语法.分享给大家供大家参考,具体如下: 文件读取 #读取文件 f = open("test.txt","r") print(f.read()) #打印文件内容 #关闭文件 f.close() 获取文件绝对路径:OS模块 os.environ["xxx"]  获取系统环境变量 os.getcwd 获取当前python脚本工作路径 os.getpid() 获取当前进程ID

  • python try 异常处理(史上最全)

    在程序出现bug时一般不会将错误信息显示给用户,而是现实一个提示的页面,通俗来说就是不让用户看见大黄页!!! 有时候我们写程序的时候,会出现一些错误或异常,导致程序终止. 为了处理异常,我们使用try...except 把可能发生错误的语句放在try模块里,用except来处理异常. except可以处理一个专门的异常,也可以处理一组圆括号中的异常, 如果except后没有指定异常,则默认处理所有的异常. 每一个try,都必须至少有一个except 在python的异常中,有一个万能异常:Exc

  • Python3.4学习笔记之类型判断,异常处理,终止程序操作小结

    本文实例讲述了Python3.4类型判断,异常处理,终止程序操作.分享给大家供大家参考,具体如下: python3.4学习笔记 类型判断,异常处理,终止程序,实例代码: #idle中按F5可以运行代码 #引入外部模块 import xxx #random模块,randint(开始数,结束数) 产生整数随机数 import random import sys import os secret = random.randint(1,10) temp = input("请输入一个数字\n")

  • python爬虫之urllib,伪装,超时设置,异常处理的方法

    Urllib 1. Urllib.request.urlopen().read().decode() 返回一个二进制的对象,对这个对象进行read()操作,可以得到一个包含网页的二进制字符串,然后用decode()解码成html源码 2. urlretrieve() 将一个网页爬取到本地 3. urlclearup() 清除 urlretrieve()所产生的缓存 4. info() 返回一个httpMessage对象,表示远程服务器的头信息 5. getcode() 获取当前网页的状态码 20

  • Python中的异常处理try/except/finally/raise用法分析

    本文实例分析了Python中的异常处理try/except/finally/raise用法.分享给大家供大家参考,具体如下: 异常发生在程序执行的过程中,如果python无法正常处理程序就会发生异常,导致整个程序终止执行,python中使用try/except语句可以捕获异常. try/except 异常的种类有很多,在不确定可能发生的异常类型时可以使用Exception捕获所有异常: try: pass except Exception, e: print Exception, ":"

  • Python异常处理知识点总结

    python提供了两个非常重要的功能来处理python程序在运行中出现的异常和错误.你可以使用该功能来调试python程序. 异常处理: 本站Python教程会具体介绍. 断言(Assertions):本站Python教程会具体介绍. python标准异常 异常名称 描述 BaseException 所有异常的基类 SystemExit 解释器请求退出 KeyboardInterrupt 用户中断执行(通常是输入^C) Exception 常规错误的基类 StopIteration 迭代器没有更

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

    所谓异常指的是程序的执行出现了非预期行为,就好比现实中的做一件事过程中总会出现一些意外的事.异常的处理是跨越编程语言的,和具体的编程细节相比,程序执行异常的处理更像是哲学.限于认知能力和经验所限,不可能达到像解释器下import this看到的python设计之禅一样,本文就结合实际使用简单的聊一聊. 0. 前言 工作中,程序员之间一言不合就亮代码,毕竟不管是代码本身还是其执行过程,不会存在二义性,更不会含糊不清,代码可谓是程序员之间的官方语言.但是其处理问题的逻辑或者算法则并非如此. 让我至今

  • 深入理解python try异常处理机制

    深入理解python try异常处理机制 #python的try语句有两种风格 #一:种是处理异常(try/except/else) #二:种是无论是否发生异常都将执行最后的代码(try/finally) try/except/else风格 try: <语句> #运行别的代码 except <名字>: <语句> #如果在try部份引发了'name'异常 except <名字>,<数据>: <语句> #如果引发了'name'异常,获得附

  • Python异常处理操作实例详解

    本文实例讲述了Python异常处理操作.分享给大家供大家参考,具体如下: 一.异常处理的引入 >>>whileTrue: try: x = int(input("Please enter a number: ")) break exceptValueError: print("Oops! That was no valid number. Try again ") Please enter a number: y Oops!That was no

  • python异常处理和日志处理方式

    今天,总结一下最近编程使用的python异常处理和日志处理的感受,其实异常处理是程序编写时非常重要的一块,但是我一开始学的语言是C++,这门语言中没有强制要求使用try...catch语句,因此我通常编写代码的时候忽略了这一块,直到开始学习java的时候,发现好多时候编写代码必须加上try...catch 模块,然而我每次都不深入理解,仅仅使用eclipse自动补全功能加上try...catch模块,或者直接在类上加入throws Exception最省事,完全不用思考. 最近在编写python

  • 深入理解python协程

    一.什么是协程 协程拥有自己的寄存器和栈.协程调度切换的时候,将寄存器上下文和栈都保存到其他地方,在切换回来的时候,恢复到先前保存的寄存器上下文和栈,因此:协程能保留上一次调用状态,每次过程重入时,就相当于进入上一次调用的状态. 协程的好处: 1.无需线程上下文切换的开销(还是单线程) 2.无需原子操作(一个线程改一个变量,改一个变量的过程就可以称为原子操作)的锁定和同步的开销 3.方便切换控制流,简化编程模型 4.高并发+高扩展+低成本:一个cpu支持上万的协程都没有问题,适合用于高并发处理

  • 深入理解python中函数传递参数是值传递还是引用传递

    目前网络上大部分博客的结论都是这样的: Python不允许程序员选择采用传值还是传 引用.Python参数传递采用的肯定是"传对象引用"的方式.实际上,这种方式相当于传值和传引用的一种综合.如果函数收到的是一个可变对象(比如字典 或者列表)的引用,就能修改对象的原始值--相当于通过"传引用"来传递对象.如果函数收到的是一个不可变对象(比如数字.字符或者元组)的引用,就不能 直接修改原始对象--相当于通过"传值"来传递对象. 你可以在很多讨论该问题

  • 深入理解Python变量与常量

    变量是计算机内存中的一块区域,变量可以存储规定范围内的值,而且值可以改变.基于变量的数据类型,解释器会分配指定内存,并决定什么数据可以被存储在内存中.常量是一块只读的内存区域,常量一旦被初始化就不能被改变. 变量命名字母.数字.下划线组成,不能以数字开头,前文有说不在赘述. 变量赋值 Python中的变量不需要声明,变量的赋值操作即是变量的声明和定义的过程.每个变量在内存中创建都包括变量的标识.名称.和数据这些信息. Python中一次新的赋值,将创建一个新的变量.即使变量的名称相同,变量的标识

  • 深入理解python中的浅拷贝和深拷贝

    在讲什么是深浅拷贝之前,我们先来看这样一个现象: a = ['scolia', 123, [], ] b = a[:] b[2].append(666) print a print b 为什么我只对b进行修改,却影响到了a呢?看过我在之前的文章中就说过:序列中保存的都是内存的引用. 所以,当我们通过b去修改里面的空列表的时候,其实就是修改内存中的同一个对象,所以会影响到a. a = ['scolia', 123, [], ] b = a[:] print id(a), id(a[0]), id(

  • 全面理解Python中self的用法

    刚开始学习Python的类写法的时候觉得很是麻烦,为什么定义时需要而调用时又不需要,为什么不能内部简化从而减少我们敲击键盘的次数?你看完这篇文章后就会明白所有的疑问. self代表类的实例,而非类. 实例来说明: class Test: def prt(self): print(self) print(self.__class__) t = Test() t.prt() 执行结果如下 <__main__.Test object at 0x000000000284E080> <class

  • 深入理解python函数递归和生成器

    一.什么是递归 如果函数包含了对其自身的调用,该函数就是递归的.递归做为一种算法在程序设计语言中广泛应用,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量.例如,要计算1-9的9位数字的乘积,直观的算法是1*2*3*4*5*6*7*8*9,如果要计算1-10000的乘积,直观的算法就难于实现出,而递归就可以很简单的实现.请看示例: def fact(n):#计算给定数字到一的乘积 i

随机推荐