python整理bugstac...

python: 错误和异常

2017-05-26  本文已影响405人  随风化作雨

一、简介

Python最强大的结构之一就是它的异常处理能力,所有的标准异常都使用类来实现,都是基类Exception的成员,都从基类Exception继承,而且都在exceptions模块中定义。Python自动将所有异常名称放在内建命名空间中,所以程序不必导入exceptions模块即可使用异常。一旦引发而且没有捕捉SystemExit异常,程序执行就会终止。异常的处理过程、如何引发或抛出异常及如何构建自己的异常类都是需要深入理解的。

二、详解

1、什么是异常

(1)错误

从软件方面来说,错误是语法或逻辑上的。语法错误指示软件的结构上有错误,导致不能被解释器解释或编译器无法编译。这些错误必须在程序执行前纠正。逻辑错误可能是由于不完整或是不合法的输入所致,在其他情况下,还可能是逻辑无法生成、计算或是输出结果需要的过程无法执行,这些错误通常分别被称为域错误和范围错误。
当Python检测到一个错误时,解释器就会指出当前流已经无法继续执行下去。这时候就出现了异常。

(2)异常

对异常的最好描述是:它是因为程序出现了错误而在正常控制流以外采取的行为。这个行为又分为两个阶段:首先是引起异常发生的错误,然后是检测和采取可能的措施阶段。

第一个阶段是在发生了一个异常条件(有时候也叫做例外的条件)后发生的。只要检测到错误并且意识到异常条件,解释器会引发一个异常。引发也可以叫做触发、引发或者生成。 解释器通过它通知当前控制流有错误发生,Python也允许程序员自己引发异常。第二阶段是:无论是 Python 解释器还是程序员引发的,异常就是错误发生的信号, 当前流将被打断,用来处理这个错误并采取相应的操作。对异常的处理发生在第二阶段, 异常引发后,可以调用很多不同的操作。可以是忽略错误(记录错误但不采取任何措施,采取补救措施后终止程序),或是减轻问题的影响后设法继续执行程序。所有的这些操作都代表一种继续,或是控制的分支。关键是程序员在错误发生时可以指示程序如何执行。

Python采用了"try/尝试"块和"catching/捕获"块的概念,而且它在异常处理方面更有"纪律性"。可以为不同的异常创建不同的处理器, 而不是盲目地创建一个"catch-all/捕获所有"的代码。

2、Python中的异常

不管是通过Python解释器执行还是标准的脚本执行,所有的错误都符合相似的格式, 这提供了一个一致的错误接口。所有错误,无论是语意上的还是逻辑上的,都是由于和Python解释器不相容导致的,其后果就是引发异常。

NameError:尝试访问一个未申明的变量, 任何可访问的变量必须在名称空间里列出, 访问变量需要由解释器进行搜索,如果请求的名字没有在任何名称空间里找到,那么将会生成一个NameError异常。

ZeroDivisionError:除数为零;SyntaxError:Python 解释器语法错误;IndexError:请求的索引超出序列范围;KeyError:请求一个不存在的字典关键字;IOError:输入/输出错误;AttributeError:尝试访问未知的对象属性。

3、检测和处理异常

异常可以通过 try 语句来检测,任何在try语句块里的代码都会被监测, 检查有无异常发生。try 语句有两种主要形式: try-except和try-finally 。这两个语句是互斥的, 即只能使用其中的一种。一个try语句可以对应一个或多个except子句,但只能对应一个finally子句,或是一个try-except-finally复合语句。可以使用try-except语句检测和处理异常,也可以添加一个可选的else子句处理没有探测到异常的时执行的代码,而try-finally只允许检测异常并做一些必要的清除工作(无论发生错误与否)。

(1)try-except语句

try-except 语句(以及其更复杂的形式)定义了进行异常监控的一段代码,并且提供了处理异常的机制。
try-except 语句语法:

try:
    try_suite                  # watch for exceptions here监控这里的异常
except Exception[, reason]:    # exception-handling code异常处理代码
    except_suite

在程序运行时,解释器尝试执行try块里的所有代码,如果代码块完成后没有异常发生,执行流就会忽略except语句继续执行。而当except语句所指定的异常发生后,会保存了错误的原因,控制流立即跳转到对应的处理器(try子句的剩余语句将被忽略)。

(2)封装内建函数

交互操作:把一个用字符串表示的数值转换为正确的数值表示形式,其中float()增加了把字符串表示的数值转换为浮点数的功能,替换以前的string.atof()。 float()对参数要求严格,例如如果参数的类型正确(字符串),其值不可转换为浮点数,那么将引发 ValueError异常;若参数是列表,因为类型不正确,所以引发一个TypeError异常。

def safe_float(obj):
    try:
        retval = float(obj)
    except ValueError:
        retval = 'could not convert non-number to float'
        return retval

(3)带有多个except 的try语句

把多个except语句连接在一起, 处理一个try块中可能发生的多种异常。

except Exception1[, reason1]:
    suite_for_exception_Exception1
except Exception2[, reason2]:
    suite_for_exception_Exception2

程序首先尝试执行try子句,如果没有错误,忽略所有的except从句继续执行。如果发生异常,解释器将在这一串处理器(except 子句)中查找匹配的异常, 如果找到对应的处理器,执行流将跳转到这里。
Python支持把except语句串连使用,分别为每个异常类型分别创建对应的错误信息,这样用户可以得到更详细的关于错误的信息。

def safe_float(obj):
    try:
        retval = float(obj)
    except ValueError:
        retval = 'could not convert non-number to float'
    except TypeError:
        retval = 'object type cannot be converted to float'
        return retval

(4)处理多个异常的except语句

except 语句可以处理任意多个异常,但要求异常被放在一个元组里。
except (Exc1[, Exc2[, ... ExcN]])[, reason]:
suite_for_exceptions_Exc1_to_ExcN
如:

def safe_float(obj):
    try:
        retval = float(obj)
    except (ValueError, TypeError):
        retval = 'argument must be a number or numeric string'
        return retval

(5)捕获所有异常

捕获所有的异常的代码:

try:
    pass
except Exception, e:
    # error occurred, log 'e', etc.

或不太推荐的方法是使用裸except子句(无任何错误信息,以后可能会不再支持):

try:
    pass
except:
    # error occurred, etc.

异常部分内容在Python 2.5有了一些变化,异常被迁移到了new-style class上,启用了一个新的"所有异常的母亲",这个类叫做 BaseException。异常的继承结构有了少许调整,KeyboardInterrupt和SystemExit被从Exception里移出,和Exception平级。
- BaseException
|- KeyboardInterrupt
|- SystemExit
|- Exception
|- (all other current built-in exceptions) 所有当前内建异常

若需要捕获所有异常,那么就可以使用新的BaseException:

try:
    pass
except BaseException, e:
    # handle all errors

注意:避免把大片的代码装入try-except中然后使用pass忽略掉错误,可以捕获特定的异常并忽略它们,或是捕获所有异常并采取特定的动作.,不要捕获所有异常,然后忽略掉它们。

(6)异常参数

异常也可以有参数,异常引发后它会被传递给异常处理器。当异常被引发后参数是作为附加帮助信息传递给异常处理器的。虽然异常原因是可选的,但标准内建异常提供至少一个参数,指示异常原因的一个字符串。异常的参数可以在处理器里忽略, 但 Python提供了保存这个值的语法。要想访问提供的异常原因,必须保留一个变量来保存这个参数,把这个参数放在except语句后,接在要处理的异常后面。except 语句的这个语法可以被扩展为:

# single exception
except Exception[, reason]:
    suite_for_Exception_with_Argument
# multiple exceptions
except (Exception1, Exception2, ..., ExceptionN)[, reason]:
    suite_for_Exception1_to_ExceptionN_with_Argument

其中reason将会是一个包含来自导致异常的代码的诊断信息的类实例。异常参数自身会组成一个元组,并存储为类实例(异常类的实例)的属性,reason将会是一个Exception类的实例。

无论reason只包含一个字符串或是由错误编号和字符串组成的元组,调用 str(reason) 总会返回一个良好可读的错误原因,因为 reason是一个类实例,这样其实是调用类的特殊方法str() 。

唯一的问题就是某些第三方或是其他外部库并不遵循标准协议, 推荐在引发自己的异常时遵循异常参数规范,用和已有Python代码一致错误信息作为传给异常的参数元组的一部分。如:若引发一个ValueError,那么最好提供和解释器引发ValueError时一致的参数信息。

(7)在应用使用我们封装的函数

处理一个文件,将其作为字符串读入,并用一个日志文件跟踪处理进程。

    #!/usr/bin/env python  
      
    def safe_float(object):  
        'safe version of float()'  
        try:  
            retval = float(object)  
        except (TypeError, ValueError), diag:  
            retval = str(diag)  
        return retval  
      
    def main():  
        'handles all the data processing'  
        log = open('cardlog.txt', 'w')  
        try:  
            ccfile = open('carddata.txt', 'r')  
        except IOError, e:  
            log.write('no txns this month\n')  
        log.close()  
            return  
      
        txns = ccfile.readlines()  
        ccfile.close()  
        total = 0.00  
        log.write('account log:\n')  
      
        for eachTxn in txns:  
            result = safe_float(eachTxn)  
            if isinstance(result, float):  
                total += result  
                log.write('data... processed\n')  
        else:  
            log.write('ignored: %s' % result)  
        print '$%.2f (new balance)' % (total)  
        log.close()  
      
    if __name__ == '__main__':  
        main()  

其中从文件中提取数据,这里的文件打开被置于try-except语句段中。内建的isinstance()函数检查结果类型,检查safe_float是返回字符串还是浮点数,任何字符串都意味着错误,表明该行不能转换为数字,同时所有的其他数字可以作为浮点数累加入total,在 main()函数的尾行会显示最终生成的余额。

(8)else子句

在try范围中没有异常被检测到时即结束前没有引发异常,然后执行else子句。如:

try:
    function()
except:
    pass
else:
    pass

(9)finally子句

finally子句是无论异常是否发生、是否捕捉都会执行的一段代码。可以将finally仅仅配合try一起使用,也可以和 try-except(else也是可选的)一起使用。
try-except-else-finally 语法的示例:

try:
    A
except MyException:
    B
else:
    C
finally:
    D

(10)try-finally语句

另一种使用finally的方式是finally单独和try连用。这个try-finally语句和try-except区别在于它不是用来捕捉异常的。作为替代,它常常用来维持一致的行为,无论 try中是否有异常触发,finally 代码段都会被执行。

try:
    try_suite
finally:          #无论如何都执行
    finally_suite

当在try范围中产生一个异常时,会立即跳转到finally语句段。当finally中的所有代码都执行完毕后,会继续向上一层引发异常。
读取数据的代码:

try:  
    ccfile = open('carddata.txt', 'r')  
    txns = ccfile.readlines()  
    ccfile.close()  
except IOError:  
    log.write('no txns this month\n') 

其中的缺陷:若按照这样的顺序发生错误,打开成功但出于一些原因readlines()调用失败,异常处理会去继续执行except中的子句,而不去尝试关闭文件。通过try-finally来优化:

ccfile = None  
try:  
    try:  
        ccfile = open('carddata.txt', 'r')  
        txns = ccfile.readlines()  
    except IOError:  
        log.write('no txns this month\n')  
finally:  
    if ccfile:  
        ccfile.close() 

另一种可选的实现切换了try-except和try-finally包含的方式:

ccfile = None  
try:  
    try:  
        ccfile = open('carddata.txt', 'r')  
        txns = ccfile.readlines()  
    finally:  
        if ccfile:  
            ccfile.close()  
except IOError:  
    log.write('no txns this month\n') 

上述方法唯一的问题是:当finally中的代码引发了另一个异常或由于return、break、continue语法而终止,会丢失原来异常的上下文信息导致原来的异常无法重新引发,除非也事先保存。

(11)try-except-else-finally:厨房一锅端

语法样式:

try:
    try_suite
except Exception1:
    suite_for_Exception1
except (Exception2, Exception3, Exception4):
    suite_for_Exceptions_2_3_and_4
except Exception5, Argument5:
    suite_for_Exception5_plus_argument
except (Exception6, Exception7), Argument67:
    suite_for_Exceptions6_and_7_plus_argument
except:
    suite_for_all_other_exceptions
else:
    no_exceptions_detected_suite
finally:
    always_execute_suite

其中else和finally都是可选的,而必须至少要有一个except子句。

4、上下文管理

(1)with 语句

另一个隐藏低层次的抽象的例子是with语句,它在Python 2.6中正式启用(以前必必须用from future import with_statement来导入它)。
with 语句的目的在于从流程图中把try、except和finally关键字和资源分配释放相关代码统统去掉,而不是像try-except-finally那样仅仅简化代码使之易用。 with 语法的基本用法:

with context_expr [as var]:
    with_suite

file和with一起使用的代码片段:

with open('/etc/passwd', 'r') as f:
    for eachLine in f:
         # ...do stuff with eachLine or f...

这段代码试图打开一个文件,如果一切正常,把文件对象赋值给f;然后用迭代器遍历文件中的每一行,当完成时关闭文件。无论在这一段代码的开始、中间还是结束时发生异常,都会执行清理的代码,此外文件仍会被自动的关闭。可以看出Python已经拿走了一堆细节,实际上只是进行了两层处理:第一,用户层 ,和in类似所需要关心的只是被使用的对象;第二,对象层,既然这个对象支持上下文管理协议,它干的也就是"上下文管理"。

5、字符串作为异常

早在Python 1.5前,标准的异常是基于字符串实现的。然而,这样就限制了异常之间不能有相互的关系,这种情况随着异常类的来临而不复存在。到1.5后,所有的标准异常都是类了,但程序员还是可以用字符串作为自己的异常的(建议使用异常类)。在2.5中,触发字符串异常会导致一个警告,2.6中捕获字符串异常会导致一个警告。

6、触发异常

有些异常由程序执行期间的错误而引发,程序员在编写API时希望遇到错误的输入时触发异常,为此Python提供了一种机制让程序员明确的触发异常,即raise语句。

(1)raise语法

raise 语句支持的参数十分灵活,语法上支持许多不同的格式。rasie一般的用法:raise [SomeException [, args [, traceback]]]。
第一个参数,SomeExcpetion是触发异常的名字。如果有,它必须是一个字符串、类或实例。如果有其他参数(arg或traceback),就必须提供SomeExcpetion.Python的标准异常。
第二个符号为可选的args(比如参数,值)来传给异常,这可以是一个单独的对象也可以是一个对象的元组。当异常发生时,异常的参数总是作为一个元组传入。如果args原本就是元组,那么就将其传给异常去处理;如果args是一个单独的对象,就生成只有一个元素的元组(就是单元素元组)。大多数情况下,单一的字符串用来指示错误的原因。如果传的是元组,通常的组成是一个错误字符串,一个错误编号,可能还有一个错误的地址比如文件等等。
最后一项参数traceback,同样是可选的(实际上很少用它)。如果有的话,则是当异常触发时新生成的一个用于异常-正常化(exception-normally)的追踪(traceback)对象。当你想重新引发异常时,第三个参数很有用(可以用来区分先前和当前的位置)。如果没有这个参数,就填写None。

(2) raise惯用法

最常见的用法为SomeException是一个类,不需要其他的参数,但如果有的话,可以是一个单一对象参数、一个参数的元组或一个异常类的实例。如果参数是一个实例,可以由给出的类及其派生类实例化(已存在异常类的子集)。若参数为实例,则不能有更多的其他参数。

(3)raise少见的惯用法

当参数是一个实例 , 该实例若是给定异常类的实例当然不会有问题,然而如果该实例并非这个异常类或其子类的实例时,那么解释器将使用该实例的异常参数创建一个给定异常类的新实例。 如果该实例是给定异常类子类的实例, 那么新实例将作为异常类的子类出现, 而不是原来的给定异常类。
如果raise语句的额外参数不是一个实例——作为替代,是一个单件(singleton)或元组,那么将用这些作为此异常类的初始化的参数列表。如果不存在第二个参数或是None,则参数列表为空。
如果SomeException是一个实例,就无需对什么进行实例化了。这种情况下,不能有额外的参数或只能是None。异常的类型就是实例的类;也就是说,等价于触发此类异常,并用该实例为参数比如:raiseinstance.class,instance。
还有一个可选的参量(args)作参数.
最后,这种不含任何参数的raise语句结构是在Python1.5中新引进的,会引发当前代码块(code block)最近触发的一个异常。如果之前没有异常触发,会因为没有可触发的异常而生成一TypeError异常。

(4)raise的不同用法

由于raise有许多不同格式有效语法(比如:SomeException 可以是类、实例或一个字符串),以下总结下rasie 的不同用法:


7、断言

断言是一句必须等价于布尔真的判定;此外,发生异常也意味着表达式为假(类似C中预处理器的assert宏),但在Python中它们在运行时构建(与之相对的是编译期判别)。 断言通过 assert 语句实现。可以简简单单的想象为raise-if语句(更准确的说是raise-if-not 语句)。测试一个表达式,若返回值是假,触发异常。
断言语句,如果断言成功不采取任何措施(类似语句),否则触发AssertionError(断言错误)的异常。assert 的语法如下:assert expression[, arguments]。AssertionError异常和其他的异常一样可以用try-except语句块捕捉,但是如果没有捕捉,它将终止程序运行而且提供一个traceback。
如同raise 语句一样,也可以提供一个异常参数给assert 命令:

    >>> try:  
    ...     assert 1 == 0, 'One does not equal zero silly!'  
    ... except AssertionError, args:  
    ...     print '%s: %s' % (args.__class__.__name__, args)  
    ...   
    AssertionError: One does not equal zero silly!  

assert 如何运作,可以通过函数类似实现(内建的变量debug在通常情况下为True):

def assert(expr, args=None):
    if __debug__ and not expr:
        raise AssertionError, args

8、标准异常

所有的异常都是内建的, 所以它们在脚本启动前或在互交命令行提示符出现时已经是可用的了。
Python内建异常:





所有的标准/内建异常都是从根异常派生的。目前有3个直接从BaseException派生的异常子类:SystemExit、KeyboardInterrupt和Exception,其他的所有的内建异常都是Exception的子类。
到了Python2.5,所有的异常的都是新风格(new-style)的类,并且最终都是BaseException的子类。从Python1.5到Python2.4.x,异常是标准的类,它们是字符串。从Python2.5开始,不再支持构建基于字符串的异常并且被正式的弃用,即不能再触发一个字符串异常了,将不能捕获它们。还有一个就是所有新的异常最终都是BaseException的子类,以便于它们有一个统一的接口。

9、sys和相关模块

(1)sys模块

另一种获取异常信息的途径是通过sys模块中exc_info()函数,此功能提供了一个3元组(3-tuple)的信息。

    >>> try:  
    ...     float('abc123')  
    ... except:  
    ...     import sys  
    ...     exc_tuple = sys.exc_info()  
    ...   
    >>> print exc_tuple  
    (<type 'exceptions.ValueError'>, ValueError('invalid literal for float(): abc123',), <traceback object at 0x7ff79c87dc20>)  
    >>> for eachItem in exc_tuple:  
    ...     print eachItem  
    ...   
    <type 'exceptions.ValueError'>  
    invalid literal for float(): abc123  
    <traceback object at 0x7ff79c87dc20>  

sys.exc_info()得到的元组中是:exc_type异常类;exc_value异常类的实例;exc_traceback追踪(traceback)对象。第三项,,是一个新增的追踪(traceback)对象,这一对象提供了的发生异常的上下文,它包含诸如代码的执行帧,异常发生时的行号等信息。在旧版本中的Python中,这三个值分别存在于sys模块,为sys.exc_type、sys.exc_value和sys.exc_traceback ,但这三者是全局变量而不是线程安全的, 建议用sys.exc_info()来代替。

(2)相关模块

模块 描述
exceptions 内建异常(永远不用导入这个模块)
contextlib 为使用 with 语句的上下文对象工具
sys 包含各种异常相关的对象和函数(见sys.ex)

三、总结

(1)Python异常不仅简化代码,而且简化整个错误管理体系,异常处理促使成熟和正确的编程。
(2)可以通过创建一个从内置的Exception类继承的类定义自己的异常,然后使用raise命令引发异常或传递异常。

上一篇下一篇

猜你喜欢

热点阅读