Python2.7-异常和工具

来自《python学习手册第四版》第七部分,而且本书发布的时候3.1还未发布,所以针对本书的一些知识会有些滞后于python的版本,具体更多细节可以参考python的标准手册。


一、异常基础(第32章)

    异常时可以改变程序中控制流程的时间。在python中异常会根据错误自动的被触发,也能由代码触发和截获。异常由四个语句处理,第一种由两种编译,二最后一张在2.6和3.0之前都是可选的扩展功能。

    1、a)try/except:捕获由python或我们引起的异常并恢复;b)try/finally:无论异常是否发生,执行清理行为;c) raise:手动在代码中触发异常;d) assert:有条件的在程序代码中触发异常;e)with/as:在2.6和后续版本中实现环境管理器(2.5中是可选的功能)。

    2、异常作为第七部分是因为需要了解类,才能编写异常。不过python的异常处理相当简单,因为它已经整合到语言本身中,成为另一种高级工具。

    3、异常可以在一个步骤内跳至异常处理器,中止开始的所有函数调用而进入异常管理器,在异常处理器中编写代码,来响应在适当时候引发的异常。异常是一种结构化的“超级goto”,异常处理器(try语句)会留下标识,并可执行一些代码。程序前进到某处代码时,产生异常,因而会使python立即跳到那个标识,而放弃留下该标识之后所调用的任何激活的函数。这个协议提供了一种固有的方式响应不寻常的事件。再者,python会立即跳到处理器的语句代码(比其他编程语言)更简单,对于可能会发生失败的函数的每次调用,就没有必要检查这些函数的状态码。

    4、异常的角色:a)错误处理,运行时检测到程序错误时,python会引发异常,可以在程序代码中捕捉和响应错误,或者忽略已发生的异常。如果忽略错误,python默认的异常处理行为将启动:停止程序,打印出错信息。如果不想使用这种默认行为,就需要写try语句来捕捉异常然后从异常中恢复:当检测到错误时,python会跳到try处理器,而程序在try之后会重新继续执行;b)事件通知:异常也可用于发出有效状态的信息,而不需要在程序间传递结果标识位,或者刻意对其进行测试。例如,搜索的程序可能在失败时引发异常,而不是返回一个整数结果代码;c)特殊情况处理:有时,发生了某种很罕见的情况,很难调整代码去处理。通常会在异常处理器中处理这些罕见的情况,从而省去编写应对特殊情况的代码;d)终止行为,try/finally语句可确保一定会进行需要的结果运算,无论程序中是否有异常;e)非常规控制流程:因为异常是一种高级的“goto”,它可以作为实现非常规的控制流程的基础。例如:虽然反向跟踪不是语言本身的一部分,但是能够通过python的异常来实现,此外需要一些辅助逻辑来退回赋值语句。

    5、和其他的核心语言话题相比,异常对python而言是相当简单的工具。

    6、默认异常处理器:假设有下面的函数:


这个函数只是通过传入的索引值对对象进行索引运算。在正常运算中,它将返回合法的索引值的结果:


如果要求这个函数对字符串末尾以后的位置做索引运算,当函数尝试执行obj[index]时,就会触发异常。python会替序列检测到超出边界的索引运算,并通过抛出(触发)内置的IndexError异常进行报告:


因为没有刻意的捕捉异常,所以将会一直向上返回到程序顶层,并启用默认的异常处理器:打印标准出错信息,这些消息包括引发的异常还有堆栈跟踪:也就是异常发生时激活的程序行和函数清单。这里的出错消息是有3.0打印出来;它随着每个版本略有不同,并且甚至随着每个交互式shell而有所不同。通过交互模式编写代码时,文件名就是"stdin"(标准输入流),表示标准的输入流。当在IDLE GUI的交互shell中工作的时候,文件名就是“pyshell”,并且会显示出源行,当没有文件的时候,文件的行号在这里没有太大的意义:


在交互模式提示符环境外启动的更为现实的程序中,顶层的默认处理器也会立刻终止程序,对简单的脚本来说,这种行为很有道理,错误通常是致命的,而档期发生时,所能做的就是查看标准出错消息。

    7、捕获异常。当不想要默认的异常行为,就需要把调用包装在try语句内,自行捕捉异常:


现在,当try代码块执行时触发异常,python会自动跳至处理器(指出引发的异常名称的except分句厦门的代码块)。像这样以交互模式进行时,在except分句执行后,我们就会回到python提示符下,在更真实的程序中,try语句不仅会捕捉异常,也会从中恢复执行:


这次,在异常捕捉和处理后,程序在捕捉了整个try语句后继续执行:这就是我们之所以得到“continuing”消息的原因。这里没看到标准出错信息,而程序也将正常运行下去。

     8、引发异常。上面的都是让python通过生成错误来引发异常,但是,脚本也可以引发异常,也就是说,异常能有python或程序引发,也能捕捉或忽略。要手动触发异常,直接执行raise语句。用户触发的异常的捕捉方式和python引发的异常一样。下面的代码是作为个例子解说:


如果没有捕捉到异常,用户定义的异常就会向上传递,直到顶层默认的异常处理器,并通过标准的出错消息终止该程序:


assert语句也可以用来触发异常,它是一个有条件的raise,主要是在开发过程中用于调试:


    9、用户定义的异常,raise语句触发python的内置作用域中定义的一个内置异常,这里也可以定义自己的新的异常,它特定与我们自己的程序。用于定义的异常能够通过类编写,它继承自一个内置的异常类:通常这个类的名称叫做Exception。基于类的异常允许脚本建立异常类型、继承行为以及附加状态信息:


    10、终止行为。try语句可以说“finally”也就是说,它可以包含finally'代码块。这看上去就像是异常的except处理器,但是try/finally的组合,可以定义一定会在最后执行时的收尾行为,无论try代码块中是否发生了异常:


这里,如果try代码块完成后没有异常,finally代码块就会执行,而程序会在整个try后继续下去。在这个例子中,这看上去不是很好的语句:似乎也可以直接在函数调用后输入print,从而完全跳过try:


不过这样写的话会有个问题:如果函数调用引发了异常,就永远到不了print。try/finally组合可以避免这个缺点:一旦异常确实在try代码块中发生时,当程序被层层剥开,将会执行finally代码块:


这里,没有看到“after try?”消息,因为当异常发生时,控制权在try/finally代码块后中断了。与其相对比的是,python跳回去执行finally的行为,然后把异常向上传播到前一个处理器(这里就是顶层的默认处理器)。如果修改这个函数中的调用,使其不触发异常,则finally程序代码依然会执行,但是程序就会在try后继续运行:


在实际应用中,try/except的组合可用于捕捉异常并从中恢复,而try/finally则很方便,用来确保无论try代码块内的代码是否发生了任何异常,终止行为一定会运行。例如,可能使用try/except来捕捉从第三方库导入的代码所引发的错误,然后以try/finally来确保关闭文件,或者终止服务器连接的调用等行为一定会执行。虽然从概念上说,是用于不同的用途,但是在2.5中,可以在同一个try语句内混合except和finally子句:finally一定会执行,无论是否有异常引发,而且也不管异常是否被except子句捕捉到。下一部分将会介绍,在使用某些类型的对象的时候,2.6和3.0提供了try/finally的一种替代,with/as运行一个对象的环境管理逻辑,来确保终止行为的发生:


虽然这个选项代码不长,它只是在处理某些对象类型的时候才适用,因此,try/finally是一种更加通用的终止结构。另一方面,with/as还运行启动操作并且支持用户定义的环境管理代码。

二、异常编码细节(第33章)

    这部分相对于前面部分的简单介绍,这里深入介绍下。特别是将介绍try、raise、assert和with语句背后的细节。虽然语句简单,但是提供了强大的工具来处理python代码中的异常。ps:异常的内容近年来有一些变化。从2.5起,finally子句可以同样出现在try语句以及except和else语句中(此前,它们不能组合)。此外,从3.0和2.6开始,新的with环境管理器语句成为正式的,并且用户定义的异常现在必须编写为类实例。此外,3.0支持raise语句和except子句的略微修改的语法。

    1、try/except/else语句。该教程把try/except/else和try/finally当成独立的语句进行介绍,因为它们是不同角色,在2.5以前都是无法合并。在2.5中,except和finally可以混在一个try语句中。try是复合语句,它的最完整的形式如下所示。首先是以try作为首行,后面紧跟着(通常)缩进的语句代码,然后是一个或多个except分句来识别要捕捉的异常,最后是一个可选的else分句。try、except以及else这些关键字会缩进在相同的层次(也就是垂直对齐)。下面是3.0中的一般格式:


在这个语句中,try首行底下的代码块代表此语句的主要动作:试着执行的程序代码。except子句定义try代码块内引发的异常的处理器,而else子句(如果写了的话)则是提供没发生异常时要执行的处理器。在这里的<data>元素和raise语句功能有关。当try语句启动时,python会标识当前的程序环境,这样一来,如果有异常发生时,才能返回这里。try首行下的语句会先执行。接下来发生的事情,取决于try代码块语句执行时是否引发异常:a)如果try代码块语句执行时的确发生了异常。python就跳回try,执行第一个符合引发异常的except子句下面的语句、当except代码块执行后(除非except代码块引发了另一异常),控制权就会到整个try语句后继续执行;b)如果异常发生在try代码块内,没有符合的except子句,异常就会向上传递到程序中的之前进入的try中,或者如果它是第一条这样的语句,就传递到这个进程的顶层;c)如果try首行底下执行的语句没有发生异常,python就会执行else行下的语句(如果有的话),控制权会在整个try语句下继续。try代码块语句可以调用写在程序其他地方的函数,异常的来源可能在try语句自身之外。

    2、try语句分句。编写try语句时,有些分句可以在try语句代码块后出现。表33-1列出所有可能形式:


至少会使用其中一种,之前介绍的比如,except分句会捕捉异常,finally分句最后一定会执行,而如果没遇上异常,else分句就会执行。在2.4中finally必须单独出现(没有else和except)。当见到raise语句时,就会探索具有额外数据的部分,它们提供了对作为异常引发的对象的访问。表33-1中第一和第四项是新的:a)except子句没列出异常名称时(except:),捕捉没在try语句内预先列出的所有异常;b)except子句以括号列出一组异常[except (e1,e2,e3):]会捕捉所有列出的任何异常。

ps:注意当try的子句没有发生异常的时候,最后的else才会执行,如果有了异常,那么else子句就不会执行。如果想要捕捉任何异常,空的except就能做到,可是当系统离开调用时,也会触发异常,这些异常是意料之外的系统异常,通常不想去捕获。这里3.0中有个替代解决方案,捕获一个名为Exception的异常几乎与一个空的except具有相同的效果,但是,忽略和系统退出相关的异常:


ps:版本差异提示:3.0要求表33-1中列出的except E as V:处理器子句形式,而不是旧的except E,V:形式,后者在2.6中仍然使用:如果使用它,会转换为前者形式。这两种替代形式在2.6中相应的编码为except(E1,E2):。由于3.0只支持as形式,不管是否使用圆括号,值都会解释为替代的异常以供捕捉。这一修改改变了作用域规则:使用新的as语法,变量V在except语句块的末尾删除。

    3、例子:默认行为。假设有厦门的模块bad.py:


程序忽略它触发的异常,python会终止这个程序,打印一个消息:


在3.0的一个shell窗口中运行它,消息包含了一个堆栈跟踪(“Traceback”)以及所引发的异常的名称和细节。堆栈跟踪按照从旧到新的顺序列出异常发生是激活状态下的所有程序的行。因为不是在交互模式提示符下工作,所以这种情况下文件和行号信息都很有用。就更大型的调试工作而言,可以用try语句捕捉异常,或者使用第3章所介绍而且会在第35章接着说的调试工具,例如pdb标准库模块。

    4、有的时候不想在python引发异常时造成程序终止,只要把程序逻辑包装在try中进行捕捉就行了,这是网络服务器这类程序很重要的功能,因为它们必须不断持续运行下去。例如,下面程序代码在python引发TypeError时就立刻予以捕捉并从中恢复,当时正试着把列表和字符串给链接起来(+运算符预期的是两边都是相同类型的序列):


当异常在函数kaboom中发生时,控制权会跳至try语句的except分句,来打印消息。因为像这样异常捕捉后就“死”了,程序会继续在try后运行,而不是被python终止:


ps:一旦捕捉了错误,控制权会在捕捉的地方继续下去(try之后),没有直接的方式可以回到异常发生的地方(这里,就是函数kaboom中)。总之,这会让异常更像是简单的跳跃,而不是函数调用:没有办法回到触发错误的代码。

    5、try/finally。a)如果try代码块没有异常,那么跳至finally代码块,然后再整个try语句后继续执行下去;b)如果try有异常,依然会回来运行finally代码块,但是接着会把异常向上传递到较高的try语句或顶层默认处理器。程序不会在try语句下继续执行。也就是说,即使发生了异常,finally代码块还是会执行,可except不同的是,finally不会终止异常,而是在finally代码块执行后,一直处于发生状态。

    6、接5的例子。下面是一个更为实际的例子,示范了这个语句的典型角色:


这段代码中,finally包含了一个文件处理函数的调用。确保不论怎么样该文件总是会关闭。这样以后的代码就可以确保文件的输出缓存去的内容已经从内存转移到磁盘了。类似的代码结构可以保证服务器链接已关闭了。当这里的函数引发异常,控制流程就会跳回,执行finally代码块并关闭文件。然后异常要么会传递到另一个try,要么就是传递至默认的顶层处理器,绝不会运行到try后的语句。如果在这里的函数没有引发异常,程序依然会执行finally代码块来关闭文件,但是,接着就是继续运行整个try之后的语句。在2.6和3.0中,异常都是类的实例。

    7、统一try/excetp/finally语句:


上面是形式的语句。ps:如果main-action中没发生异常,而在else-block中发生了,finally还是会执行的,只是执行完之后控制权跳转到其他地方(另一个try或者默认的顶层处理器)。

    8、统一try语句语法:


其中,else和finally是可选的,可能会有0个或多个except,但是,如果出现一个else的话,必须至少有一个except。实际上,该try语句包含两个部分:带有一个可选的else的except,以及(或)finally。下面的方式更准确的描述了这一组合的语句语法形式(方括号表示可选,星号表示0个或多个):


由于这些规则,只有至少有一个except的时候,else才能够出现,并且总是可能混合except和finally,而不管是否有一个else,也可能混合finally和else,但只有在一个except也出现的时候(尽管except可能会忽略一个异常名以捕获所有的并运行一条raise语句,以重新引发当前的异常)。如果违反了这些顺序规则中的任意一条,在代码运行之前,python将会引发一个语法错误异常。

    9、通过嵌套合并finally和except。这里的是2.5之前的技术:


相比较于当前的来说,比较晦涩难懂,所以推荐当前的技术。

    10、合并try的例子。下面是文件mergedexc.py的四种常见的场景,通过print语句来说明其意义:



下面是在3.0中的输出结果:


这个例子使用main-action里的内置表达式,来触发异常(或不触发),而且利用了python总是会在代码运行时检查错误的事实。下面介绍如何手动引发异常。

    11、raise语句。这是显式的触发异常,raise语句的组成是:raise关键字,后面跟着可选的要引发的类或者类的一个实例:


在2.6和3.0中,异常总是类的实例。因此,这里第一个raise形式是最常见的,直接提供一个实例,要么是在raise之前创建的,要么是在raise语句中自带的。如果传递一个类,python调用不带构造函数参数的类,以创建被引发的一个实例:这个格式等同于在类引用后面添加圆括号。最后的形式重新引发最近引发的异常;它通常用于异常处理器中,以传播已经捕获的异常。比如下面的两个例子,对于内置异常,下面两种形式是对等的,都会引发指定的异常类的一个实例,但是,第一种形式隐式的创建实例:


也可以提前创建实例,因为raise语句接受任何类型的对象引用,如下两个就像前面两个一样引发了IndexError:


当引发一个异常的时候,python把引发的实例与该异常一起发送。如果一个try包含一个名为except name as X:子句,变量X将会分配给引发中所提供的实例:


as在try处理器中是可选的(如果忽略它,该实例直接不会分配给一个名称),但是,包含它将使得处理器能够访问实例中的数据以及异常类中的方法。这种模式对于用类编写的用户定义的异常也同样有效,例如,下面的代码,传递异常类构造函数参数,该参数通过分配的实例在处理器中变得可用:


不管如何指定异常,异常总是通过实例对象来识别,并且大多数时候在任意给的的时刻激活。一旦异常在程序中某处由一条except子句捕获,它就死掉了,除非由另一个raise语句或错误重新引发它。

    12、利用raise传递异常。raise语句不包括异常名称或额外数据值时,就是重新引发当前异常。如果需要捕捉和处理一个异常,又不希望异常在程序代码中死掉时,一般会使用下面的形式:


通过这种方式执行raise时,会重新引发异常,并将其传递给更高层的处理器。

    13、3.0的异常链:raise from。3.0(不是2.6)也允许raise语句拥有一个可选的from子句:


当使用from的时候,第二个表达式指定了另一个异常类或实例,它会附加到引发异常的__cause__属性。如果引发的异常没有捕获,python把异常也作为标准出错信息的一部分打印出来:



当在一个异常处理器内部引发一个异常的时候,隐式的遵从类似的过程:前一个异常附加到新的异常__context__属性,并且如果该异常未捕获的话,再次显示在标准出错消息中。ps:3.0不再支持raise Exc, Args形式。该形式在2.6中仍然可用,在3.0中使用raise Exc(Args)示例创建调用形式。2.6中等价的逗号形式是遗留的语法,为了与现在废弃的基于字符串的异常类型兼容,并且在3.0中也是废弃的。如果使用的话,会转换成3.0的调用形式。正如前面看到的,一个raise Exc 形式总是允许的,它在两个版本中都会转换为raiseExc()形式,调用无参数的类构造函数。

    14、assert语句。assert可视为条件式的raise语句,该语句形式为:


执行起来就像如下的代码:


如果test计算为假,python就会引发异常:data项(如果提供了的话)是异常的额外数据。就像所有异常,引发的AssertionError异常如果没有被try捕捉,就会终止程序,在此情况下数据项将作为出错消息的一部分显示。assert语句是附加的功能,如果使用-0 python命令行标识位,就会从程序编译后的字节码中移除,从而优化程序。AssertionError是内置异常,而__debug__标志位是内置变量名,除非有使用-0标志,否则自动设为1(真值)。使用类似python-0 main.py的一个命令行来在优化模式中运行,并关闭assert。

    15、例子:手机约束条件(但不是错误)。assert通常是用来作为验证开发期间程序状况的,显示时,其出错消息正文会自动包括源代码的行信息,以及列在assert语句中的值,考虑文件asserter.py:


assert几乎都是用来收集用户定义的约束条件,而不是捕捉内在的程序设计错误。因为python会自行收集程序的设计错误。通常来说,没必要写assert去捕捉超出索引值,类型不匹配以及除数为零之类的事情:


这类assert一般是多余的。因为python会在遇见错误时自动引发异常,让python来做。另一个例子在第28章的抽象超类例子里。

    16、with/as环境管理器。在2.6和3.0中引入了一种新的异常相关的语句:with及其可选的as子句。这个语句的设计是为了和环境管理器对象(支持新的方法协议)一起工作。这一功能在2.5中也可选的使用,用一条如下形式的import来激活:


简单来说,with/as语句的设计是作为常见的try/finally用法模式的替代方案。就像try/finally语句,with/as语句也是用于定义必须执行的终止或“清理”行为,无论处理步骤中是否发生异常。不过和try/finally不同的是,with语句支持更丰富的基于对象的协议,可以为代码块定义支持进入和离开动作。python以环境管理器强化一些内置工具,例如,自动自行关闭的文件,以及对锁的自动上锁和解锁,同时可以用类编写自己的环境管理器。

    17、基本使用:with语句的基本格式如下:


这里的expression要返回一个对象,从而支持环境管理协议。如果选用as子句存在时,此对象也可返回一个值,赋值给变量名variable。ps:variable并非赋值为expression的结果。expression的结果是支持环境协议的对象,而variable则是赋值为其他的东西。然后,expression返回的对象可在with-block开始前,先执行启动程序,并且在该代码块完成后,执行终止程序代码,无论该代码块是否引发异常。有些内置的python对象已得到强化,支持了环境管理协议,因此可以用于with语句。例如,文件对象有环境管理器,可在with代码块后自动关闭文件,无论是否引发异常:


在这里,对open的调用,会返回一个简单文件对象,赋值给变量名myfile。可以用一般的文件工具来使用myfile:文件迭代器会在for循环内逐行读取。然而,此对象也支持with语句所使用的环境管理协议。在这个with语句执行后,环境管理机制保证由myfile所引用的文件对象会自动关闭,即使处理该文件时,for循环引发了异常也是如此。尽管文件对象在垃圾回收时自动关闭,然而,并不总是能够很容易的知道会何时发生。with语句的这种用法作为一种替代,允许我们确定在一个特定代码块执行完毕后会发生关闭。正如前面看到的,可以使用更同样而明确的try/finally语句来实现类似的效果,但是,这需要4行管理代码而不是1行:


这里不讨论python的多线程模块,但是这些模块所定义的锁和条件变量同步对象也可以和with语句一起使用,因为它们支持环境管理协议:


在这里,环境管理机制保证锁会在代码块执行前自动获得,并且一旦代码块完成就释放,而不管异常输出是什么。decimal模块(第5章)也使用环境管理来简化存储和保存当前小数配置环境(定义了赋值计算时的精度和取整的方式):


在这条语句运行后,当前线程的环境管理器状态自动恢复到语句开始之前的状态。要使用try/finally做到同样的事情,需要提前保存环境并手动恢复它。

    18、环境管理协议。可以自行编写环境管理器。要实现环境管理器,使用特殊的方法来接入with语句,该方法属于运算符重载的范畴。用在with语句中对象所需的接口有点复杂,而多数程序员只需知道如何使用现有的环境管理器,不过,对那些可能想写新的环境管理器的工作创造者来说,可以先快速浏览其中的细节:

以下是with语句实际的工作方式:a)计算表达式,所得到的对象称为环境管理器,它必须有__enter__和__exit__方法;b)环境管理器的__enter__方法会被调用。如果as子句存在,其返回值会赋值给as子句中的变量,否则,直接丢弃;c)代码块中嵌套的代码会执行;d)如果with代码块引发异常,__exit__(type,value,traceback)方法就会被调用(带有异常细节)。这些也是由sys.exc_info返回的相同值。如果此方法返回值为假,则异常会重新引发。否则,异常会终止。正常情况下异常是应该被重新引发,这样的话才能传递到with语句之外;e)如果with代码块没有引发异常,__exit__方法依然会被调用,其type,value以及traceback参数都会以None传递。

    19、接18。这里是一个协议的示范。下面定义一个环境管理器对象,跟踪其所用的任意一个with语句内with代码块的进入和退出:


ps:这个类的__exit__方法返回False来传播该异常。删除那里的return语句也有相同效果,因为默认的函数返回值None,按定义也是False。此外,__enter__方法返回self,因为默认的函数返回值None,按定义也是False。此外,__enter__方法返回self,作为赋值给as变量的对象。在其他情况下,这里可能会返回完全不同的对象。运行时,环境管理器以__enter__和__exit__跟踪with语句代码块的进入和离开。如下是实际在3.0中运行的脚本(也能够在2.6中运行,但是会打印出额外的元组圆括号):


ps:参考标准手册来了解更多细节,新的contextlib标准模块提供其他工具来编写环境管理器。ps:(本书发布的时候3.1还未发布)在即将发布的3.1中,with语句也可以使用新的逗号语法指定多个(有时候叫做嵌套)环境管理器。例如,在下面的例子中,当语句块退出的时候,两个文件的退出操作都会自动运行,而不管异常输出什么:


可以列出任意数目的环境管理器项,并且多个项目和嵌套的with语句一样的工作。通常python3.1(及其之后的版本)代码:


等同于如下的代码,它们在3.1,3.0和2.6下都有效:


三、异常对象(第34章)

       python把异常的概念一般化,在2.6和3.0中,内置异常和用户定义的异常都可以通过类实例对象来标识,虽然这表示必须使用面向对象编程来在程序中定义新的异常,类和oop通常提供了几个优势:a)提供类型分类,对今后的修改有了更好的支持,以后增加新的异常时,通常不需要在try语句中进行修改;b)它们附加了状态信息,异常类提供了存储在try处理器中所使用的环境信息的合理地点:这样的话,可以拥有状态信息以及可调用的方法,并且可以通过实例进行读取;c)它们支持继承,基于类的异常允许参与继承层次,从而可以获得并定制共同的行为,例如,继承的显示方法可提供通用的出错消息的外观。因为有这些差异,所以基于类的异常支持了程序的演进和较大系统,所有内置异常都是类组织成继承树。在3.0中,用户定义的异常继承自内置异常超类,由于这些超类为打印和状态保持提供了有用的默认值,所以编写用户定义的异常的任务也涉及理解这些内置超类的作用。ps:2.6和3.0都要求异常通过类来定义,此外,3.0要求异常类派生自BaseException内置异常超类,而不管是直接还是间接,大多数程序都继承自这个类的Exception子类,以支持针对常规异常类型的全捕获处理器,在一个处理器中指定它将会捕获大多数程序应该捕获的所有内容,2.6也允许独立的标准类来充当异常,但是它要求新式类派生自内置异常类,这与3.0相同。

    1、在2.6和3.0之前有两种不同的方式来定义异常,现在只有一种:它从语言中删除了为了实现向后兼容而保留的大量冗余内容。由于旧的方式有助于说明为什么异常称为今天的样子,而且也真的不可能完全擦除上百万人使用了近二十年的教程的历史。这里先看看异常的过去

    2、接1,在2.6和3.0之前,可以使用类实例和字符串对象来定义异常。基于字符串的异常在2.5中就发布了废弃警告,并且在2.6和3.0中删除了。(所以不要用这部分)字符串异常很容易使用,任何字符串都可以做到,它们根据对象标识来匹配,而不是根据值(也就是说,使用is,而不是==):


对于较大的程序和代码维护来说,这种形式并不像类那么好,所以放弃了。

    3、基于类的异常。字符串是定义异常的简单方式,相比较于类多了一些优点,最主要的是,类可以让组织的异常分类,使用和维护更灵活。再者,类可附加异常的细节,而且支持继承。字符串异常和类异常的主要差别在于,引发的异常在try语句中except子句匹配时的方式不同:

a)字符串异常以简单对象识别来匹配:引发的异常是由python的is测试来匹配except子句的;

b)类异常是由超类关系进行匹配的:只要except子句列举了异常的类或其任何超类名,引发的异常就会匹配该子句。

也就是说,当try语句的except子句列出一个超类时,就可以捕捉该超类的实例,以及类树中所有较低位置的子类的实例,结果就是,类异常支持异常层次的架构:超类变成分类的名称,而子类变成这个分类中特定种类的异常。except子句列出一个通用的异常超类,就可捕捉整个分类中的各种异常:任何特定的子类都可以匹配;字符串异常没有这样的概念:它们是通过简单的对象标识匹配,没有直接的方式将它们组织到更为灵活的领域或分组。使得异常处理器以一种难以做出修改的方式与异常集合匹配;基于类的异常也能够更好的支持了异常状态信息(附加到实例上),而且可以让异常参与继承层次(从而获得通用的行为)。由于它们提供类和oop一般性的所有优点,比起字符串的异常来说,提供更强大的替代方案。

    4、来看看在代码中类异常是如何应用的,下列classexc.py文件中,定义了一个名为General的超类,以及两个子类Specific1和Specific2.这个例子说明异常分类的概念:General是分类的名称,而其两个子类是这个分类中特定种类的异常。捕捉General的处理器也会捕捉其任何子类,包括Specific1和Specific2:



这段代码相当直接,不过有些实现细节需要注意:

a)Exception超类。用来构建异常分类树的类拥有很少的需求,实际上,在上面这个例子中,它们主要是空的,其主体不做任何事情而直接通过。不过,需要注意这里顶层的类是如何从内置的Exception类继承的。这在3.0中是必需的;2.6也允许独立的经典类充当异常,但是它要求新式类派生自内置异常类,这和在3.0中一样。由于Exception提供了一些有用的行为,随后才会遇到这些行为,因此,在这里不能使用它们;但是,在任何python版本中,从它那里继承是个好主意;

b)引发实例。在上面代码中,调用类来创建raise语句的实例。在类异常模式中,总是引发和捕获一个类实例对象。如果我们在一个raise中列出了类名而没有圆括号,那么python调用该类而没有构造函数参数为我们产生一个实例。异常实例可以在该raise之前创建,就像这里所做的一样,或者在raise语句自身中创建;

c)捕获分类。这段代码包含一些函数,引发三个类实例使其成为异常,此外,有个顶层try会调用那些函数,并捕捉General异常(同一个try也会捕捉两个特定的异常,因为它们是General的子类);

d)异常细节。在下一章详细说说异常处理器sys.exc_info调用:这是一种抓取最近发生异常的常用方式。简单来说,对基于类的异常而言,其结果中的第一个元素就是引发异常类,而第二个是实际引发的实例。这里的except子句捕获了一个分类中所有的类,在这一的一条通用的except子句中,sys.exc_info是决定到底发生了什么的一种方式。在这一特别情况下,它等价于获取实例的__class__属性。

    5、接4,正如在下一章看到的,sys.exc_info方法通常也捕获所有内容的空的except子句一起使用。最后一点需要说的是,当捕获了一个异常,我么可以确定该实例是except中列出的类的一个实例,或者是其更具体的子类中的一个。因此,实例的__class__属性也给出了异常类型。例如,如下的变体和前面的例子起着相同的作用:


由于__class__可以像这样使用来决定引发的异常的具体类型,因此sys.exc_info对于空的except子句更有用,否则的话,没有一种方式来访问实例及其类。此外,更使用的程序通常根本不必关注引发了哪个具体的异常,通过一般调用实例的方法,我们自动把修改后的行为分派给引发的异常。

    6、为什么使用异常。给出厦门的形式,在except子句的括号内列出字符串异常名称的清单,可以达到和上面一样的效果:


这种方法对于废弃的字符串异常模式也是有效的。不过对大型或多层次的异常而言,在一个except子句中使用类捕捉分类,会比列出一个分类中的每个成员更为简单。此外,可以新增子类扩展异常层次,而不会破坏现有的代码。例如,这里给出个例子,编写一个数值计算库,并给出两个异常:除数为零以及数值溢出。在文档中指出这些是异常,可以通过库引发这两个异常,同时在代码中将异常定义为简单字符串:


现在,当人们使用库的时候,一般会在try语句内,把对函数或类的调用包装起来,从而捕捉两个异常(如果他们没捕捉异常,库的异常会终止代码):


当这样使用差不多6个月或者多久的时候,你想做修改,在这个过程中,发现比如退位(underflow)也会出错,所以增加新的一个字符串异常:


不过,也许当发布代码时,用户会发现它们如果要明确列出异常,就得回去修改每处调用我们的库的地方,来引入新增的异常名:


用户为了方面,编写了空的except子句来捕捉所有的可能的异常:


可是这样却会捕捉到不想要的异常:内存耗尽,键盘终端(ctrl-c),系统退出甚至自己的try块中的代码录入错误,都将触发异常。类异常可以完全修复这种难题,不是将库的异常定义为简单的一组字符串,而是安排到类树中,有个共同的超类来包含整个类型:


这样的话,库用户只需列出共同的超类(也就是分类),来捕捉库的所有异常,无论是现在还是以后:


当回来修改代码时,作为共同超类的新的子类来增加新的异常:


结果就是用户代码捕捉库的异常依然保持正常工作,没有改变。事实上,可以在未来任意新增、删除以及修改异常,只要客户端使用的是超类的名称,就和异常集中的修改无关,也就是维护更容易了。

    7、要理解上述的这些用法,首先需要看看用户定义的异常类与它们所继承自的内置异常是如何相关的。内置Exceptiopn类。python自身能够引发的所有的内置异常,都是预定义的类对象。此外,内置异常通常通过一般的超类分类以及具体的子类形态组织成的层次,很像之前学过的异常类树。在3.0中,所有熟悉的异常(例如,SyntaxError)都是预定义的类,可以作为内置变量名,可以作为builtin模块中的内置名称使用(在2.6中,位于__builtin__,并且也是标准库模块exceptions的属性)。此外,python把内置异常组织成层次,来支持各种捕捉模式:

a)BaseException.,异常的顶级根类,这个类不能当作是由用户定义的类直接继承的(使用Exception)。它提供了子类所继承的默认的打印和状态保持行为。如果在这个类的一个实例上调用str内置函数(例如,通过print),该类返回创建实例的时候所传递的构造函数参数的显示字符串(或者如果没有参数的话,是一个空字符串)。此外,除非子类替代了这个类的构造函数,在实例构造时候传递给这个类的所有参数都将作为一个元组存储于其args属性中;

b)Exception。与应用相关的异常的顶层根超类。这是BaseException的一个直接子类,并且是所有其他内置异常的超类,除了系统退出事件类之外(SystemExit、KeyboardInterrupt和GeneratorExit)。几乎所有的用户定义的类都应该继承自这个类,而不是BaseException。当遵从这一惯例的时候,在一条try语句的处理器中指明Exception,会确保程序将捕获除了系统退出事件之外的所有异常,通常该事件是允许通过的。实际上,Exception变成了try语句中的一个全捕获,并且比一条空的except更精确。

c)ArithmeticError。所有数值错误的超类(并且是Exception的一个子类)。

d)OverflowError。识别特定的数值错误的子类。

其他等等,可以在python pocket reference或python库手册这样的帮助文本中进一步阅读详细内容。注意,异常类树在3.0和2.6中略有不同。还要注意,只有在2.6中,可以在exception模块(在3.0中删除了)的帮助文本中看到类树:


    8、内置异常分类。内置类树可让我们选择处理器具体或通用的程度。例如,内置异常ArithmeticError是如OverflowError和ZeroDivisionError这样的更为具体的异常的超类。在一条try中列出ArithmeticError,将会捕获所引发的任何类型的数值错误;只列出OverflowError时,就只会拦截这种特定类型的错误,而不能捕捉其他的异常。与之类似的是,因为Exception是python中所有应用程序级别的异常的超类,通常可以使用它作为一个全捕获,其效果与一条空的except很类似,但是它运行系统退出异常而像平常那样通过:


这在2.6中通常没用,因为编写为经典类的独立的用户定义异常,不要求必须是Exception根类的子类。这在3.0中不可靠,因为它要求所有的类都派生自内置异常。即便在3.0中,这种方案会像空的except一样遭遇大多数相同的潜在陷阱,就像前一章说的,他可能拦截用于其他地方的异常,并且可能掩盖了真正的变成错误。

    9、默认打印和状态。内置异常还提供了默认打印显示和状态保持,它往往和用户定义的类所需的逻辑一样多。除非重新定义了类继承自它们的构造函数,传递给这些类的任何构造函数参数都会保存在实例的args元组属性中,并且当打印该实例的时候自动显示(如果没有传递构造函数参数,则使用一个空的元组和显示字符串)。这说明了为什么传递给内置异常类的参数会出现在出错消息中,当打印实例的时候,附加给实例的任何构造函数参数就会显示:


对于用户定义的异常也是如此,因为它们继承了其内置超类中存在的构造函数和显示方法:



注意,该异常实例对象并非字符串自身,但是,当打印的时候,使用第29章介绍的__str__运算符重载协议来提供显示字符串:要连接真正的字符串,执行手动转换:str(X)+"string"。尽管这种自动状态和现实支持本身是有用的,但对于特定的显示和状态保持需求,总是可以重新定义Exception子类中的__str__和__init__这样的继承方法,下面的10介绍如何做到的。

    10、定制打印显示。正如第9中看到的,默认情况下,捕获并打印基于类的异常的实例的时候,它们会显示我们传递给类构造函数的任何内容:


当没有捕获异常的时候,如果异常作为一条出错消息的一部分显示,这个继承的默认显示模式也会使用:


对于很多用途来说,这已经足够了。要提供一个更加定制的显示,可以在类中定义两个字符串标识重载方法中的一个(__repr__或__str__),来返回想要为异常显示的字符串。如果异常被捕获并打印,或者异常到达默认的处理器,方法返回的字符串都将显示:



这里要注意的一点是,通常为此目的必须重新定义__str__,因为内置的超类已经有一个__str__方法,并且在大多数环境下(包括打印),__str__优先于__repr__.。如过定义了一个__repr__,打印将会很乐意的调用超类的__str__。对于未捕获的异常,方法返回的内容都包含在出错消息中,并且打印异常的时候显示化。这里,方法返回一个硬编码的字符串来说明,但是,它也可以执行任意的文本处理,可能附加到实例对象的状态信息。第11部分介绍状态信息选项。

    11、定制数据和行为。除了支持灵活的层级,异常类还提供了把额外状态信息存储为实例属性的功能。正如前面看到的,内置异常超类提供了一个默认的构造函数,它自动把构造函数参数存储到一个名为args的实例元组属性中。尽管默认的构造函数对于很多情况都适用,但为了满足更多的定制需求,可以提供一个自己的构造函数。此外,类可以定义在处理器中使用的方法,来提供预先编码的异常处理逻辑。

    12、接11.当引发一个异常的时候,可能会跨越任意的文件界限,触发异常的raise语句和捕获异常的try语句可能位于完全不同的模块文件中。在一个全局变量中存储额外的细节通常是不行的,因为try语句可能不知道全局变量位于哪个文件中。在异常自身中传递额外的状态信息,这允许try语句更可靠的访问它。正如之前看到的,当引发一个异常的时候,python随着异常传递类实例对象。在try语句中的代码,可以通过在一个except处理器中的as关键字之后列出一个额外的变量,来访问引发的异常。这提供了一个自然的钩子,以用来为处理器提供数据和行为。例如,解析数据文件的一个程序可能通过引发一个异常实例来标识一个格式化错误,而该实例用关于错误的额外细节来填充:


在这里的except子句中,对引发异常的时候所产生的实例的一个引用分配给了X变量。这使得能够通过定制的构造函数来访问附加给该实例的属性。尽管可能依赖于内置超类的默认状态保持,它与我们的应用程序几乎不相关:


    13、提供异常的方法。除了支持特定于应用程序的状态信息,定制构造函数还更好的支持用于异常对象的额外信息。也就是说,异常类也可以定义在处理器中调用的方法。例如,下面的代码添加了一个方法,它使用异常状态信息把错误记录到一个文件中:



运行的时候,这段脚本把出错消息写入一个文件中,以响应异常处理器中的方法调用:


在这一的一个类中,方法(如logerror)也可能继承自超类,并且实例属性(例如line和file)提供了一个地方来保存状态信息,状态信息提供了额外环境用于随后的方法调用。此外,异常类可以自由的定制和扩展继承的行为。换句话说,由于它们是用类定义的,所以第六部分的oop的特性,对于异常来说也是可用的。

四、异常的设计(第35章)

    这部分包括了异常设计的话题以及常用例子的集合,再加上这一部分的陷阱,(每一部分的最后应该都有python的工具的介绍,比如pydoc什么的)。

    1、嵌套异常处理器。上面的例子都只使用了单一的try语句来捕捉异常,如果try中还有try,那么怎样?也就是try调用一个会执行另一个try的函数,也就是嵌套。python会在运行时将try语句放入堆栈,这样就能够理解了。当发生异常时,python会回到最近进入、具有相符except分句的try语句。因为每个try语句都会留下标识,python可检查堆栈的标识,从而跳回到较早的try。这种处理器的嵌套化,就是之前说的异常向上传递至较高的处理器的意思:这类处理器就是在程序执行流程中较早进入的try语句。图35-1说明了嵌套的try/except语句在运行时所发生的事情。进入try代码块的代码量可能很大(例如,它可能包含了函数调用),而且通常会启用正在监视相同异常的其他代码。当异常最终引发时,python会跳回到匹配该异常,最近进入的try语句,执行该语句的xcept分句,然后在try语句后继续下去。一旦异常被捕捉,其生命就结束:控制权不会跳回所有匹配这个异常、相符的try语句;只有第一个try有机会对它进行处理,如图35-1所示,函数func2中的raise语句会把控制权返还func1中的处理器,然后程序再在func1中继续下去:


与之对比的,当try/finally语句嵌套且异常发生时,每个finally代码块都会执行:python会持续把异常往上传递到其他try语句上,最终可能达到顶层默认处理器。如图35-2,finally子句不会终止异常,而是指明异常传播过程中,离开每个try语句之前要执行的代码。如果异常发生时,有很多try/finally都在活动,它们就都会运行,除非有个try/except在这个过程中捕捉某处该异常:


也就是印发异常时,程序去向何方完全取决于异常在何处发生:这是脚本运行时控制流程的函数,而不仅仅是其语法。异常的传递,基本上就是回到处理先前进入但尚未离开的try。只要控制权碰到相符except子句,传递就会停止,而通过finally子句时就不会。

    2、例子:控制流程嵌套。下面是模块文件nestexc.py定义了两个函数。action2是写成要触发异常(做数字和序列的加法),而action1把action2调用封装在try处理器内,以捕捉异常:


那么,文件底端的顶层模块代码,也在try处理器中包装了action1调用,当action2触发TypeError异常时,就有两个激活的try语句:一个在action1内,另一个在模块文件顶层。python会挑选并执行具有相符except,最近try,而在这个例子中就是action1中的try。异常最后的所在之处,取决于程序运行时的控制流程。因此,想要知道要去哪,就知道现在在哪。就这个例子而言,异常在哪进行处理是控制流程的函数,而不是语句的语法。然而,可以用语法把异常处理器嵌套化。

    3、例子:语法嵌套化。第33章讨论新的统一后的try/except/finally语句时,就像前面提到的,从语法上有可能让try语句通过其源代码中的位置来实现嵌套:


这段代码只是像之前的那个例子一样(行为也相同),设置了相同的处理器嵌套结构、。实际上,语法嵌套的工作就像图35-1和35-2所描绘的一样。唯一的差别就在于,嵌套处理器实际上是嵌入try代码块中,而不是写在其他被调用的函数中,例如,嵌套的finally处理器会因一个异常而全部启动,无论是语法上的嵌套,或者因运行时流程经过代码中某个部分:


参考图35-2有关这段代码运行的图形说明。效果是一样的,不过函数逻辑变成了嵌套语句。有关语法嵌套更有用的例子,可以看下下面的文件except-finally.py:


此代码在异常引发时,会对其进行捕捉,而且无论是否发生异常,都会执行finally终止动作。其效果就像单个try语句内结合except和finally(在2.5及其以后的版本中):



就像在第33章见到的,2.5时,except和finally子句可以混合在相同try语句中。这也是的本节讲的某些语法嵌套变得不再必要,虽然依然可用,但可能是出现在2.5版本以前的代码中,而且可作为执行其他的异常处理行为的技术。

    4、异常的习惯用法:异常不总是错误。在python中,所有错误都是异常,可是不是所有的异常都是错误。例如,在第9章看到,文件对象读取方法会在文件末尾时返回空字符串。与之对比的是,内置的input 函数在每次调用时,则是从标准输入串流sys.stdin读取一行文字,并且在文件末尾时引发内置的E0FError(这一功能在2.6中叫做raw_input)。和文件方法不同的是,这个函数并不是返回空字符串:input的空字符串是指空行。除了E0FError的名称,这个异常在这种环境下也只是信号而已,不是错误。因为有这种行为,除非文档末尾应该终止脚本,否则,input通常会出现在try处理器内,并嵌入循环内,如下列代码:


其他内置异常都是类似的信号,而不是错误,例如,调用sys.exit()并在键盘上按下Ctrl-C,会分别引发SystemExit和KeyboardInterrupt。python也有一组内置异常,代表警告,而不是错误。其中有些代表了正在使用不推荐的(即将退出的)语言功能的信号。(可参考库手册有关内置异常的说明,以及warning模块相关的警告)。

    5、函数信号条件和raise。用户定义的异常也可引发非错误的情况。例如,搜索程序可以写成找到相符者时引发异常,而不是为调用者返回装填标志来拦截。在下面的代码中,try/except/else处理器做的就是if/else返回值的测试工作:


更通常的情况是,这种代码结构,可用于任何无法返回警示值(sentinel value)以表明成功或失败的函数。例如,如果所有对象都是可能的有效返回值,就不可能以任何返回值来代表不寻常的情况。异常提供一种方式来传达结果信号,而不使用返回值:


因为python核心是动态类型和多态的,所以通常倾向于使用异常来发出这类情况的信号,而不是警示性的返回值。

    6、关闭文件和服务器链接。确保一个特殊代码块的终止操作的更通用和显式的方式是try/finally语句:


2.6和3.0中的一些对象使得这更为容易:提供由with/as语句运行的环境管理器,从而为我们自动终止或关闭对象:


那么,选哪种好呢?通常,这取决于我们的程序。与try/finally相比,环境管理器更为隐式,它与python通常的设计哲学背道而驰。环境管理器肯定也不太常见,它们通常只对选定的对象可用,并且编写用户定义的环境管理器来处理通用的终止需求,比编写一个try/finally更为复杂。另一方面,使用已有的环境管理器,比使用try/finally需要更少的代码,如前面的例子所示。此外,环境管理器协议除了支持退出动作,还支持进入动作。尽管try/finally可能是更加广为应用的技术,环境管理器可能更适合可以使用它们的地方,或者可以允许它们的额外复杂性的地方。

    7、在try外进行调试。也可以利用异常处理器,取代python的默认顶层异常处理行为。在顶层代码中的外层try中包装整个程序(或对它调用),就可以捕捉任何程序执行时会发生的异常,因此可破坏默认的程序终止行为。下面的代码中,空的except子句会捕捉任何程序执行时所引发的而未被捕捉到的异常。要取得所发生的实际异常,可以从内置sys模块取出sys.exc_info函数的调用结果。这会返回一个元组,而元组之前两个元素会自动包含当前异常的类和引发的实例对象:


这种结构在开发期间经常会使用,在错误发生后,仍保持程序处于激活状态:这样可以执行其他测试,而不用重新开始。测试其他程序时,也会用到它,就像下一节描述的。

    8、运行进程中的测试。可以在测试驱动程序的应用中结合刚才所见到的一些编码模式,在同一进程中测试其他代码:


在这里的testdriver函数会循环进行一组测试调用(在这个例子中,模块testapi是抽象的)。因为测试案例中未被捕捉的异常,一般都会终止这个测试驱动程序,如果想在测试失败后让测试进程继续下去,就需要在try中包装测试案例的调用。就像往常一样,空的except会捕捉由测试案例所产生的没有被捕捉的异常,而其使用sys.exc_info把该异常记录到文件内。没有异常发生时(测试成功情况),else分句就会执行。对于作为测试驱动运行在同一个进程的函数、模块以及类,而进行测试的系统而言,这种形式固定的代码是很经典的。然而,在实际应用中,测试可能会比这里所演示的更为复杂。例如,要测试外部程序时,要改为检查程序启动工具所产生的状态代码或输出。例如,标准库手册所谈到的os.system和os.open(这一类工具一般不会替外部程序中的错误引发异常。事实上,测试案例可能会和测试驱动并行运行)。

    9、关于sys.exc_info。sys.exc_info通常允许一个异常处理器捕获对最近引发的异常的访问。当使用空的except子句来盲目的捕获每个异常以确定引发了什么的时候,这种方式特别有用:


如果没有处理器正在处理,就返回包含了三个None值的元组。否则,将会返回(type、value和traceback):a)type是正在处理的异常的异常类型;b)value是引发的异常类实例;c)traceback是一个traceback对象,代表异常最初发生时所调用的堆栈(参考标准traceback模块到文档,来获得可以和这个对象共同使用的对象工具,从而可以手动产生出错消息的工具)。正如在前一章看到的,当捕获异常分类超类的时候,sys.exc_info对于确定特定的异常类型很有用。由于在这种情况下,也可以通过as子句所获取的实例的__class__属性来获得异常类型,sys.exc_info如今主要由空的except使用:


也就是说,使用实例对象的接口和多态,往往比测试异常类型更好的方法,可以为每个类定义异常方法并通用的运行:


通常,在python中太具体可能会限制代码的灵活性。ps:在2.6中,旧的工具sys.exc_type和sys.exc_value依然可以用于获得最近异常的类型和值,但是只能替整个进程管理单个的全局异常。这两个名称在3.0中已经删除。新的更倾向于使用的sys.exc_info()调用在2.6和3.0中都可以使用,它记录每个线程的异常信息,因此是线程专有的方式。当然,当在python中使用多线程时,这种区别才更重要。

    10、与异常有关的技巧。在异常这一部分中,大多数常见的陷阱都来自于设计问题,大致来说,python的异常在使用上都很简单,异常背后的真正的技巧在于,确定except子句要多具体或多通用,以及try语句中要包装多少代码。

    11、接11,应该包装什么。从理论上说,可以在脚本中把所有的语句都包装在try中,但是这样不明智(这样的话,try语句也需要包装在try语句中)。这是设计上的问题,不是语言的问题:

a)经常会失败的运算一般都应该包装在try语句内。例如,和系统状态衔接的运算(文件开启、套接字调用等)就是try的主要候选者;

b)尽管这样,上面的规则有些特例:在简单的脚本中,会希望这类运算失败时终止程序,而不是被捕捉或者被忽略。如果错误更大,那么更要这样。python中的错误会产生有用的出错信息(如果不是崩溃的话),而且这通常就是所期望的最好结果;

c)应该在try/finally中实现终止动作,从而保证它们的执行,除非环境管理器作为一个with/as选项可用。这个语句的形式可以执行代码,无论异常是否发生;

d)偶尔,把对大型函数的调用包装在单个try语句内,而不是让函数本身零散在诺干try语句内。这样更方便。这样的话,函数中的异常就会往上传递到调用周围的try,而也可以减少函数的代码量。

我们所编写的程序种类可能会影响处理异常的代码的量。例如,服务器一般都必须持久的运行,所以,可能需要try语句捕捉异常并从中恢复。不过较简单的一次性脚本,通常会完全忽略异常的处理,因为任何步骤的失败都要求关闭脚本。

    12、捕捉太多:避免空except语句。空except子句会捕捉try代码块中代码执行时所引发的每一个异常:


捕捉的也包含无关的系统异常,而这些我们却不需要去关注的也会被捕捉到。例如,当控制权到达顶层文件的末尾时,脚本是正常的退出。然而,python也提供内置sys.exit(statuscode)调用来提前终止。这实际上是引发内置的SystemExit异常来终止程序,使try/finally可以在离开前执行,而程序的特殊类型可拦截该事件。因此,try带空except时,可能会不知不觉阻止重要的结束,如下面的文件exiter.py:



可能会无法预期运算中可能发生的所有的异常种类。使用前一章介绍的内置异常类,在这种特定情况下会有帮助,因为Exception超类不是SystemExit的一个超类:


在其他情况下,这种方案并不比空的except子句好,因为Exception是所有内置异常(除了系统退出事件以外)之上的一个超类,它仍然有潜力捕获程序中其他地方的异常。最糟糕的情况是,空except和捕获Exception类也会捕捉一般程序设计错误,但这类错误多数时候都应让其通过的。事实上,这两种技术都会有效关闭python的错误报告机制,使得代码中的错误难以发现。例如,下面的代码:


这里的代码的编写者假设,对字典做字典运算时,唯一可能发生的错误就是键错误。但是,因为变量名myditcionary拼写错误(mydictionary),python会为未定义的变量名的引用引发NameError,但处理器会默默的捕捉并忽略这个异常。事件处理器错误填写了字典错误的默认值,导致了程序出错。如果这件事是发生在离读取值的使用很远的地方,就会变成一项很复杂的调试任务。

    13、捕捉过少:使用基于类的分类。另一方面,处理器也不能太具体化。当在try中列出特定的异常时,就只捕捉实际所列出的事件,可是如果系统需要演化的时候,需要回头在代码的其他地方,加上这些新的异常。在前一章就说过这个问题,例如下面处理器是吧MyExcept1和MyExcept2看作是正常的情况,并把其他的一切视为错误。如果未来增加了MyExcept3,就会视为错误并对其进行处理,除非更新异常列表:


不过如果和第33章讨论过的基于类的异常,可以让这种陷阱消失,就像见到的,如果捕捉一般的超类,就可以在未来新增和引发更为特定的子类,而不用手动扩展except分句的列表:超类会变成可扩展的异常分类:


这个道理就是异常处理器不要过于一般化,也不要太具体。要明智的选择try语句所包装的代码量,特别是在较大系统中,异常规则也应该是整体设计的一部分。

    14、核心语言总结。python工具集。从这里开始,以后的python生涯大部分时间就是熟练的掌握应用级的python编程的工具集。而且这是一项持续不断的任务。例如,标准库包含了几百个模块,而公开领域提供更多的工具。有可能花个十年甚至更多的时间去研究所有这些工具,尤其是新的工具还在不断的涌现。一般来说,python提供了一个有层次的工具集:

a)内置工具,和字符串、列表以及字典这些内置类型,会让编程更迅速。

b)python扩展。可以编写自己的函数、模块以及类来扩展python

c)已编译的扩展。比如使用c或者cpp这样的外部语言所编写的模块进行扩展。

因为python将其工具集分层,可以决定程序任务要多么的深入这个层次:可以让简单的脚本使用内置工具,替较大系统新增python所编写的扩展工具,并且为高级工作编写编译好的扩展工具。表35-1总结python程序员可用的内置或现有的功能来源,而有些话题可能会用剩下的python生涯时间来探索:


大型项目的开发工具:PyDoc和文档字符串;PyChecker和Pylint;PyUnit;doctest;IDE;配置工具(profile模块);调试器(pdb调试器);发布的选择(py2exe、PyInstaller以及freeze都可打包字节码以及python虚拟机,从而成为“冻结二进制”的独立的可执行文件,也就是不需要目标机器上有安装python');优化选项(Psyco系统提供实时的编译器,Shedskin系统提供python对cpp的翻译器);对于大型项目的提示(23、33、30、15、21、24、17、19章可以深入研究)。

posted @ 2015-05-30 20:01  仙守  阅读(3512)  评论(0编辑  收藏  举报