史上最全Python学习笔记(基于《Python学习手册(第4版)》)——Part7 异常和工具

Chap32 异常基础

五种异常处理语句:

  • try/except:捕捉由Python或程序员引起的异常并恢复
  • try/finally:无论异常是否发生,都执行清理行为
  • raise:手动在代码中触发异常
  • assert:有条件地在程序代码中触发异常
  • with/as:在Python2.6及后续版本中实现环境管理器

为什么使用异常

异常可以在一个步骤内跳至异常处理器,中止开始的所有函数调用进而进入异常管理器。在异常处理器中编写代码,来响应在适当时候引发的异常。

异常是一种结构化的“超级goto”。异常处理器会留下标识,并可执行一些代码。程序前进到某处代码时,产生异常,因而会使Python立即跳到那个标识,而放弃留下该标识之后所调用的任何激活的函数。这个协议提供了一种固有的方式响应不寻常的事件。再者,因为Python会立即跳到处理器的语句代码更简单——对于可能会发生失败的函数的每次调用,通常就没有必要检查这些函数的状态码。

异常的角色

在Python中,异常可以用于各种用途,以下是它最常见的几种角色。

  • 错误处理
  • 事件通知
  • 特殊情况处理
  • 终止行为
  • 非常规控制流程

异常处理:简明扼要

默认异常处理器

如果在代码中没有可以捕捉某个可能发生的异常,那么当异常发生时,会一直向上返回到程序顶层,并启用默认的异常处理器:即打印标准出错消息(sys.stderr)。这些消息包括引发的异常还有堆栈跟踪:也就是异常发生时激活的程序行和函数清单。

捕获异常

在更多的情况下,异常由默认的异常处理器来处理并不是所期望的。例如,服务器程序一般需要在内部错误发生时依然保持工作。如果不想要默认的异常行为,就需要把调用包装在try语句内,自行捕捉异常。

try:
    fetcher(x,4)
except IndexError:
    print('got exception')

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

def catcher():
    try:
        fetcher(x,4)
    except IndexError:
        print('got exception')
    print('continuing')

引发异常

异常能由Python或程序来引发,也能捕捉或忽略。要手动触发异常,直接执行raise语句。用户触发的异常的捕捉方式和Python引发的异常一样。

try:
    raise IndexError
except IndexError:
    print("got exception")

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

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

用户定义的异常

用户定义的异常能够通过类编写,它继承自一个内置的异常类:通常这个类的名称叫做Exception。基于类的异常允许脚本建立异常类型、继承行为以及附加状态信息。

class Bad(Exception):
    pass
def demo():
    raise Bad()
try:
    demo()
except Bad:
    print("got Bad")

终止行为

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

try:
    fetcher(x,3)
finally:
    print('after fetch')

Chap33 异常编码细节

try/except/else语句

try是符合语句,它的最完整形式如下所示。首先是以try为首行,后面紧跟着(通常)缩进的语句代码,然后是一个或多个except分句来识别要捕捉的异常,最后是一个可选的else分句。try、except、else这些关键字会缩进在相同的层次(也就是垂直对齐)。

try:
    <statement1>  # Run this main action first
except <name1>:
    <statement2>  # Run if name1 is raised during tyr block
except <name2,name3>:
    <statement3>  # Run if any of these exceptions occur
except <name4> as <data>:
    <statement3>  # Run if name4 is raised and get instance raised
except:
    <statement4>  # Run for all (other) exceptions raised
else:
    <statement5>  # Run if no exception was raised during try block

在这个语句中,try首行地下的代码块代表此语句的主要动作:试着执行的程序代码。except子句定义try代码块内引发的异常的处理器,而else子句(如果编写了的话)则是提供没发生异常时要执行的处理器。在这里的data元素和raise语句功能有关,之后会进行讨论。

以下是try语句的运行方式。当try语句启动时,Python会标识当前的程序环境,这样一来,如果异常发生时,才能返回这里。try首行下的语句就会先执行。接下来会发生什么事情,则取决于try代码块语句执行时是否引发异常。

  • 如果try代码块语句执行时的确发生了异常,Python就跳回try,执行第一个符合引发异常的except子句下面的语句。当except代码块执行之后(除非except代码块引发了另一异常),控制器就会到整个try之后的语句继续执行。
  • 如果异常发生在try代码块中,没有符合的except子句,异常就会向上传递到程序中的之前进入的try中,或者如果它是第一条这样的语句,就传递到这个进程的顶层(这会使Python终止这个程序并打印默认的出错消息)。
  • 如果try首行底下执行的语句没有发生异常,Python就会执行else行下的语句(如果有的话),控制器会在整个try语句下继续。

换句话说,except分句会捕捉try代码块执行时发生的任何异常,而else子句只在try代码块执行时不发生异常的情况下才会执行。

except子句是专注于异常处理器的:捕捉只在相关try代码块中的语句所发生的异常。尽管这样,因为try代码块语句可以调用卸载程序其他地方的函数,异常的来源可能在try语句自身之外。第35章探讨嵌套化时,会再多介绍一些关于这方面的内容。

try语句分句

try语句分句形式:

分句形式说明
except:捕捉所有(其他)异常类型
except name:只捕捉特定的异常
except name,value:捕捉所列的异常和额外的数据(或实例)
except(name1,name2):捕捉任何列出的异常
except(name1,name2),value:捕捉任何列出的异常,并获取额外的数据
else:如果没有异常,就运行
finally:总是运行此代码块

如果try语句块中引发了异常,Python就会回到try,并搜索第一个和异常名称相符的except。Python会从头到尾以及由左至右查看except子句,然后执行第一个相符的except下的语句。如果没有符合的,异常会向这个try外传递。注意:这只有当action中没有发生异常时,else才会执行,当没有相符except的异常发生时,则不会执行。

空的except子句是一种通用的功能:因为这是捕捉任何东西,可让处理器通用化或具体化。再某些场合下,比起列出try中所有可能异常来说,这种形式反而更方便一些。

不过,空except也会引发一些设计的问题:尽管方便,也可能捕捉和代码无关、意料之外的系统异常,而且可能以外拦截其他处理器的异常。例如,在Python中,即便是系统离开调用,也会触发异常,而程序员通常会想让这些事件通过。这一部分末尾会再谈这个陷阱。目前而言,要小心使用。

Python3.0引入了一个替代方案来解决这些问题之一——捕获一个名为Exception的异常几乎与一个空的except具有相同的效果,但是,忽略和系统退出相关的异常:

try:
    action()
except Exception:
    ...

这与空的except具有大多相同的便利性,但是,几乎同样具有危险性。在下一章学习异常类的时候会介绍这种形式如何发挥其魔力。

try/else分句

如果没有else,是无法直到控制流程(没有设置和检查布尔标志)是否已经通过try语句,因为没有异常引发或者因为异常发生且被处理过。

try:
    ...run code...
except IndexError:
    ...handle exception...
# Did we get here because the try failed or nor?

就像循环内的else子句让退出原因更加明显,else分句也为try中提供了让所发生的事情更为明确而不模糊的语法。

try:
    ...run code...
except IndexError:
    ...handle exception...
else:
    ...no exception occurred...

把else代码块中的程序移进try代码块中,也几乎能模拟else分句。不过,这可能会造成不正确的异常分类。如果“没有异常发生”这个行为触发了IndexError,就会视为try代码块的失败,因此错误地触发try地下的异常处理器(微妙,但真实)改为使用明确的else分句,可以让逻辑更为明确,保证except处理器只会因包装在try中的代码真正的失败而执行,而不是为else情况中的行为失败而执行。

例子:默认行为

def gobad(x,y):
    return x/y

def gosouth(x):
    print(gobad(x,0))

gosouth(1)  # 程序忽略了触发的异常,Python会终止这个程序并打印一个消息
---------------------------------------------------------------------------

ZeroDivisionError                         Traceback (most recent call last)

<ipython-input-1-ed4b2a88dc02> in <module>()
      5     print(gobad(x,0))
      6 
----> 7 gosouth(1)


<ipython-input-1-ed4b2a88dc02> in gosouth(x)
      3 
      4 def gosouth(x):
----> 5     print(gobad(x,0))
      6 
      7 gosouth(1)


<ipython-input-1-ed4b2a88dc02> in gobad(x, y)
      1 def gobad(x,y):
----> 2     return x/y
      3 
      4 def gosouth(x):
      5     print(gobad(x,0))


ZeroDivisionError: division by zero
try:
    gosouth(1)
except ZeroDivisionError:
    print('除数不能为零!')
除数不能为零!

例子:捕捉内置异常

如果不想在Python引发异常事时造成程序终止,只要把程序逻辑包装在try中进行捕捉就行了。这是网络服务器这类程序很重要的功能,因它们必须不断持续运行下去。

def kaboom(x,y):
    print(x+y)
    
try:
    kaboom([0,1,2],"spam")
except TypeError:
    print('Hello world!')
print('resuming here')
Hello world!
resuming here

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

try/finally语句

如果try语句中包含了finally子句,Python一定会在try语句后执行其语句代码块,无论try代码块执行时是否发送了异常。其一般形式如下所示:

try:
    <statements>
finally:
    <statements>

当想确定某些程序代码执行后,无论程序的异常行为如何,有个动作一定会发生,那么这种形式就很有用。在实际应用中,这可以让你定义一定会发生的清理动作,例如,文件关闭以及服务器断开连接等。

例子:利用try/finally编写终止行为

class MyError(Exception):pass

def stuff(file):
    raise MyError()
    
file=open('test.txt','w')
try:
    stuff(file)
finally:
    file.close()
    print('not reached')
not reached



---------------------------------------------------------------------------

MyError                                   Traceback (most recent call last)

<ipython-input-3-1f466edc3fb1> in <module>()
      6 file=open('test.txt','w')
      7 try:
----> 8     stuff(file)
      9 finally:
     10     file.close()


<ipython-input-3-1f466edc3fb1> in stuff(file)
      2 
      3 def stuff(file):
----> 4     raise MyError()
      5 
      6 file=open('test.txt','w')


MyError: 

统一try/except/finally语句

如今,可以在同一个try语句中混合finally、except以及else子句。

try:
    main-action
except Exception1:
    handler1
except Exception2:
    handler2
...
else:
    else-block
finally:
    finally-block

这个语句中的main-action代码块会先执行。如果该程序代码引发异常,那么所有except代码块都会逐一测试,寻找与抛出的异常相符的语句。如果一你发的异常是Exception1,则会执行handler1代码块;如果引发的是Exception2,则执行handler2代码块;以此类推。如果美欧引发异常,则会执行else-block代码块。无论发生了什么,总是会执行finally-block代码块。

统一try语句语法

当像这样的组合的时候,try语句必须有一个except或一个finally,并且其部分的顺序必须如下所示:try->except->else->finally

其中,else和finally是可选的,可能会有0或多个except,但是,如果出现一个else,则必须至少一个except。实际上,该try语句包含两个部分,带有一个可选的else的except,以及(或)finally。

实际上,下面的方式更准确地描述了这一组合的语句语法形式(方括号表示可选,星号表示0或多个):

try:  # Format1
    statements
except [type [as value]]:
    statements
[except [type [as value]]:
    statements]*
[else:
    statements]
[finally:
    statements]
    
try:  # Format2
    statements
finally:
    statements

合并try的例子

以下示范了合并的try语句的执行情况。

sep='-'*32+'\n'
print(sep+'EXCEPTION RAISED AND CAUGHT')

try:
    x='spam'[99]
except IndexError:
    print('except run')
finally:
    print('finally run')
print('after run')

print(sep+'NO EXCEPTION RAISED')

try:
    x='spam'[3]
except IndexError:
    print('except run')
finally:
    print('finally run')
print('after run')

print(sep+'NO EXCEPTION RAISED,WITH ELSE')

try:
    x='spam'[3]
except IndexError:
    print('except run')
else:
    print('else run')
finally:
    print('finally run')
print('after run')

print(sep+'EXCEPTION RAISED BUT NOT CAUGHT')
try:
    x=1/0
except IndexError:
    print('except run')
finally:
    print('finally run')
print('after run')
--------------------------------
EXCEPTION RAISED AND CAUGHT
except run
finally run
after run
--------------------------------
NO EXCEPTION RAISED
finally run
after run
--------------------------------
NO EXCEPTION RAISED,WITH ELSE
else run
finally run
after run
--------------------------------
EXCEPTION RAISED BUT NOT CAUGHT
finally run



---------------------------------------------------------------------------

ZeroDivisionError                         Traceback (most recent call last)

<ipython-input-4-0adc33ae6147> in <module>()
     34 print(sep+'EXCEPTION RAISED BUT NOT CAUGHT')
     35 try:
---> 36     x=1/0
     37 except IndexError:
     38     print('except run')


ZeroDivisionError: division by zero

raise语句

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

raise <instance> # Raise instance of class
raise <class>  # Make and raise instance of class
raise  # Reraise the most recent exception

第一个raise形式是最常见的,直接提供一个实例,要么是在raise之前创建的,要么是在raise语句中自带的。如果传递一个类,Python调用不带构造函数参数的类,以创建被引发的一个实例;这个格式等同于在类引用后面添加圆括号。最后的形式重新引发最近引发的异常;它通常用于异常处理器中,以传播已经捕获的异常。

对于内置异常,如下两种形式是对等的,都会引发指定的异常类的一个实例,但是第一种形式隐式地创建实例:

raise IndexError
raise IndexError()

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

exc=IndexError()
raise exc

excs=[IndexError,TypeError]
raise excs[0]

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

try:
    ...
except IndexError as X:
    ...

as在try处理器中是可选的(如果忽略它,该实例直接不会分配给一个名称),但是,包含它将使得处理器能够访问实例中的数据以及异常类中的方法。

这种模式对于用类编写的用户定义的异常类也同样有效——例如,如下的代码,传递异常类构造函数参数,该参数通过分配的实例在处理器中变得可用:

class MyExc(Exception):pass
...
raise MyExc('spam')
...
try:
    ...
except MyExc as X:
    print(X.args)

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

利用raise传递异常

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

try:
    raise IndexError('spam')
except IndexError:
    print('propagating')
    raise
propagating



---------------------------------------------------------------------------

IndexError                                Traceback (most recent call last)

<ipython-input-5-30f65ef43030> in <module>()
      1 try:
----> 2     raise IndexError('spam')
      3 except IndexError:
      4     print('propagating')
      5     raise


IndexError: spam

通过这种方式执行raise时,会重新引发异常,并将其传递给更高层的处理器(或者顶层的默认处理器,它会停止程序,打印出标准出错消息)。注意传递给异常类的参数是如何出现在出错消息中的,将会在下一章中了解为什么会这样。

Python3.0异常链:raise from

Python3.0也允许raise语句拥有一个可选的from子句:

raise exception from otherexception

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

try:
    1/0
except Exception as E:
    raise TypeError('Bad!') from E
---------------------------------------------------------------------------

ZeroDivisionError                         Traceback (most recent call last)

<ipython-input-6-145730a4b1e7> in <module>()
      1 try:
----> 2     1/0
      3 except Exception as E:


ZeroDivisionError: division by zero


The above exception was the direct cause of the following exception:


TypeError                                 Traceback (most recent call last)

<ipython-input-6-145730a4b1e7> in <module>()
      2     1/0
      3 except Exception as E:
----> 4     raise TypeError('Bad!') from E


TypeError: Bad!

当在一个异常处理器内部引发一个异常的时候,隐式地遵从类似的过程:前一个异常附加到新的异常的__context__属性,并且如果该异常未捕捉的话,再次显示在标准出错消息中。这是一个高级的并且多少还有些含糊的扩展,因此,请参阅Python的手册以了解详细内容。

assert语句

Python还包括了assert语句,这种情况有些特殊。这是raise常见使用模式的语法简写,assert可视为条件式的raise语句。该语句的形式为:

assert <test>,<data>

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

if __debug__:
    if not <test>:
        raise AssertionError(<data>)

换句话说,如果raise计算为假,Python就会引发异常:data项(如果提供了的话)是异常的额外数据。就像所有异常,引发的AssertionError异常如果没被try捕捉,就会终止程序,在此情况下数据项将作为出错消息的一部分展示。

assert语句是附加的功能,如果使用-O Python命令行标志位,就会从程序编译后的字节码中移除,从而优化程序。AssertionError是内置异常,而__debug__标志位是内置变量名,除非有使用-O标志,否则自动设置为1(真值)。使用类似Python -O main.py的一个命令行来在优化模式中运行,并且关闭assert。

例子:收集约束条件(但不是错误)

Assert语句通常是用于验证开发期间程序状况的。显示时,其出错消息正文会自动包括源代码的行信息,以及列在assert语句中的值。

def f(x):
    assert x<0,'x must be negative'
    return x**2

try:
    x=f(1)
finally:
    print(x)
m



---------------------------------------------------------------------------

AssertionError                            Traceback (most recent call last)

<ipython-input-11-fcecd8df4135> in <module>()
      4 
      5 try:
----> 6     x=f(1)
      7 finally:
      8     print(x)


<ipython-input-11-fcecd8df4135> in f(x)
      1 def f(x):
----> 2     assert x<0,'x must be negative'
      3     return x**2
      4 
      5 try:


AssertionError: x must be negative

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

这类assert一般都是多余的:因为Python会在预见错误时自动引发异常,让Python替你把事情做好了就行了。另一个assert常见用法例子,可以参考第28章的抽象超类例子。在那里,使用assert让未定义的调用失败并打印消息。

with/as环境管理器

简而言之,with/as语句的设计是作为常见try/finally用法模式的替代方案。就像try/finally语句,with/as语句也是用于定义必须执行的终止或“清理”行为,无论处理步骤中是否发送异常。不过,和try/finally不同的是,with语句支持更丰富的基于对象的协议,可以为代码块定义支持进入和离开的动作。

Python以还击管理器强化一些内置工具,例如,自动自行关闭的文件,以及对锁的自动上锁和开锁,程序员也可以用类编写自己的环境管理器。

基本使用

with语句的基本格式如下:

with expression [as value]:
    with-block

在这里,exprission要返回一个对象,从而支持环境管理协议(稍后会谈到这个协议的更多内容)。如果选用的as子句存在时,此对象也可以返回一个值,赋值给变量名value。

注意:value并非赋值为expression的结果。expression的结果是支持环境协议的对象,而value则是赋值为其他的东西。然后,expression返回的对象可在with-block开始前,先执行启动程序,并且在该代码块完成后,执行终止程序代码,无论该代码块是否引发异常。

有些内置的Python对象已经得到强化,支持环境管理协议,因此可以用于with语句。例如,文件对象有环境管理器,可以在with代码块后自动关闭文件,无论是否引发异常:

with open(r'C:\misc\data') as myfile:
    for line in myfile:
        print(line)
        ...more code here...

在这里,对open的调用,会返回一个简单文件对象,赋值给变量名myfile。可以用一般的文件工具来使用myfile:就此而言,文件迭代器会在for循环内逐行读取。

然而,此对象也支持with语句所使用的环境管理协议。在这个with语句执行后,环境管理机制保证由myfile所引用的文件对象会自动关闭,即使该处理文件时,for循环引发了异常也是如此。

尽管文件对象在垃圾回收时自动关闭,然而,并不总是能够容易地知道会何时发送。with语句的这种用法作为一种替代,允许程序员确定在一个特定代码块执行完毕后会发生关闭。正如前面所看到的,可以使用更通用而明确的try/finally语句来实现类似的效果,但是,这里需要4行代码而不是1行:

myfile=open(r'C:\misc\data')
try:
    for line in myfile:
        print(line)
        ... more code here...
finally:
    myfile.close()

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

lock=threading.Lock()
with lock:
    # critical section of code
    ... access shared resources...

在这里,环境管理机制保证锁会在代码块执行前自动获得,并且一旦代码块完成就释放,而不管异常输出是什么。

decimal模块(小数模块)也使用环境管理器来简化存储和保存当前小数配置环境(定义了赋值计算时的精度和取整的方式)。

with decimal.localcontext() as ctx:
    ctx.prec=2
    x=decimal.Decimal('1.00')/decimal.Decimal('3.00')

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

环境管理协议

尽管一些内置类型带有环境管理器,程序员还是额可以自己编写一个。要实现环境管理器,使用特殊的方法来接入with语句,该方法属于运算符重载的范畴。用在with语句对象中所需要的接口有点复杂,而多数程序员只需知道如何使用现有的还击管理器。

以下是with语句实际的工作方式:

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

来看一个示范,以下定义一个环境管理器对象,跟踪其所用的任意一个with语句内with代码块的进入和退出。

class TraceBlock:
    def message(self,arg):
        print('running',arg)
    def __enter__(self):
        print('starting with block')
        return self
    def __exit__(self,exc_type,exc_value,exc_tb):
        if exc_type is None:
            print('exited normally\n')
        else:
            print('raise an exceptino!',exc_type)
            return False
with TraceBlock() as action:
    action.message('test 1')
    print('reached')

with TraceBlock() as action:
    action.message('test 2')
    raise TypeError
    print('not reached')
starting with block
running test 1
reached
exited normally

starting with block
running test 2
raise an exceptino! <class 'TypeError'>



---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-14-16d218488674> in <module>()
     17 with TraceBlock() as action:
     18     action.message('test 2')
---> 19     raise TypeError
     20     print('not reached')


TypeError: 

Chap34 异常对象

基于类的异常有如下特点

  • **提供类型分类,对今后的修改有更好的支持。**以后增加新异常时,通常不需要在try语句中进行修改。
  • **它们附加了状态信息。**异常类提供了存储在try处理器中所使用的环境信息的合理地点:这样的话,可以拥有状态信息以及可调用的方法,并且可以通过实例进行读取。
  • **它们支持继承。**基于类的异常允许参与继承层次,从而可以获得并定制共同的行为。例如,继承的显示方法可以提供通用的出错信息的外观。

异常:回到未来

基于类的异常

类可以让程序员组织的异常分类,使用和维护起来更灵活。其次,类可以附加异常的细节,而且支持继承。

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

也就是说,当try语句的except子句列出一个超类时,就可以捕捉该超类的实例,以及类树中所有较低位置的子类的实例。结果就是,类异常支持异常层次的架构:超类变成分类的名称,而子类变成这个分类中特定种类的异常。except子句列出一个通用的异常超类,就可捕捉整个分类中的各种异常:任何特定的子类都可以匹配。

类异常例子

以下的代码例子中,定义了一个名为general的超类,以及两个子类Specific1和Specific2。这个例子说明异常分类的概念:General是分类的名称,而其两个子列Specific1和Specific2是这个分裂中特定种类的异常。捕捉General的处理器也会捕捉其任何子类,包括Specific1和Specific2。

class General(Exception):pass
class Specific1(General):pass
class Specific2(General):pass

def raiser0():
    X=General()
    raise X
    
def raiser1():
    X=Specific1()
    raise X

def raiser2():
    X=Specific2()
    raise X

for func in (raiser0,raiser1,raiser2):
    try:
        func()
    except General:
        import sys
        print('caught:',sys.exc_info()[0])
caught: <class '__main__.General'>
caught: <class '__main__.Specific1'>
caught: <class '__main__.Specific2'>

以上代码十分直接,但这里有一些实现细节需要注意:

Exception超类

用来构建异常分类树的类拥有很少的需求——在这里,且通常,其主体不做任何事情而直接通过。注意,这里顶层的类是如何从内置的Exception类继承的,这在PY3中式必须的。由于Exception提供了一些有用的行为,在随后才会遇到这些行为,一次你这里不使用它们。但在任何Py版本中,从它那里继承是个好主意。

引发实例

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

捕获分类

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

异常细节

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

正如在下一章将看到的,sys.exc_info方法通常也与捕获所有内容的空的except子句一起使用。最后一点值得进一步说明。当捕获了一个异常,就可以确定该实例是except种列出的类的一个实例,或者是其更具体的子类中的一个。因此,实例的__class__属性也给出了异常类型。例如,如下的变体和前面的例子起着同样的作用。

class General(Exception):pass
class Specific1(General):pass
class Specific2(General):pass

def raiser0():raise General()
def raiser1():raise Specific1()
def raiser2():raise Specific2()
    
for func in (raiser0,raiser1,raiser2):
    try:
        func()
    except General as X:
        print('caught:',X.__class__)
caught: <class '__main__.General'>
caught: <class '__main__.Specific1'>
caught: <class '__main__.Specific2'>

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

为什么使用异常类

对大型或多层次的异常而言,在一个except子句中使用类捕捉分类,会比列出一个分类中的每个成员更为简单。因此,可以新增子类扩展异常层次,而不会破坏现有的代码。

把自己编写的库中的异常安排到一个类树中,让一个共同的超类来包含整个类型,如此,用户只需列出共同的超类(也就是分类),来捕捉库的所有异常,无论现在还是以后。这样在未来修改代码时,作为共同超类的新的子类就能够增加新的异常。

结果就是用户代码捕捉库的异常依然保持正常工作,没有改变。事实上,可以在未来任意新增、删除以及修改异常,只要客户端使用的是超类的名称,就和异常几种的修改无关。换句话说,对于维护的问题来说,类异常提供了更好的答案。

内置Exception类

在Python3中,所有熟悉的异常(例如,SyntaxError)其实都是预定义的类,可以作为内置变量名,可以作为builtin模块中的内置名称使用。此外,Python把内置异常组织成层次,来支持各种捕捉模式。

BaseException

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

Exception

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

ArithmeticError

所有数值错误的超类,并且这是Exception的一个子类。

OverflowError

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

其他的内置异常类,可以在Python Pocket Reference或Python手册这样的帮助文本中进一步阅读关于这个结构的内容。

内置异常分类

内置类树可以让程序员选择处理器具体或通用的程度。选择某个特定的内置异常分类(比如ArithmeticError),在try中列出它,则将只会捕获所引发的这个异常分类下的所有异常,而不能捕捉其他异常。

与之相似的是,因为Exception是Python中所有应用程序级别的异常的超类,通常可以将其作为一个全捕获,其效果与一条空的except很相似,但它语序系统退出异常而像平常那样通过。

try:
    action()
except Exception:
    ...handle all opplication exceptions...
else:
    ...handle no-exception case...

当然这一技术在Python3中不会更为可靠,因为它要求所有的类都派生自内置异常。即便在PY3中,这种方案会像空的except一样遭遇到大多数相同的潜在陷阱,就像之前一张所介绍的那样——它可能拦截用于其他地方的异常,并且可能掩盖了真正的编程错误。因此在下一章的“陷阱”部分将会回顾。

默认打印和状态

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

raise IndexError
---------------------------------------------------------------------------

IndexError                                Traceback (most recent call last)

<ipython-input-3-55a00e7db5b5> in <module>()
----> 1 raise IndexError


IndexError: 
raise IndexError('spam')
---------------------------------------------------------------------------

IndexError                                Traceback (most recent call last)

<ipython-input-4-53b7e456dbfa> in <module>()
----> 1 raise IndexError('spam')


IndexError: spam
I=IndexError('spam')
I.args
('spam',)

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

class E(Exception):pass

try:
    raise E('spam')
except E as X:
    print(X,X.args  # Displays and saves constructor arguments
spam ('spam',)

注意,该异常实例对象并非字符串本身,但是当打印的时候,使用29章处介绍的__str__运算符重载协议来提供显示字符串;要连接真正的字符串,执行手动转化:str(X)+‘string’。

尽管这种自动转台和实现支持本身是有用的,但对于特定的显示和状态保持需求,总是可以重新定义Exception子类中的__str__和__init__这一的继承方法。下一小节介绍如何做到这一点。

定制打印显示

默认情况下,捕获并打印基于类的实例的时候,它们会显示编程时传递给类构造函数的任何内容:

class MyBad(Exception):pass

try:
    raise MyBad('Sorry--my mistake!')
except MyBad as X:
    print(X)
Sorry--my mistake!

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

raise MyBad('Sorry--my mistake!')
---------------------------------------------------------------------------

MyBad                                     Traceback (most recent call last)

<ipython-input-8-264d8a9614b8> in <module>()
----> 1 raise MyBad('Sorry--my mistake!')


MyBad: Sorry--my mistake!

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

class MyBad(Exception):
    def __str__(self):
        return 'Always look on the bright side of life...'
    
try:
    raise MyBad()
except MyBad as X:
    print(X)
Always look on the bright side of life...
raise MyBad()
---------------------------------------------------------------------------

MyBad                                     Traceback (most recent call last)

<ipython-input-10-ff3b7d17d944> in <module>()
----> 1 raise MyBad()


MyBad: Always look on the bright side of life...

这里需要注意的细微一点是,通常要为此目的而重新定义__str__,因为内置的超类已经有一个__str__方法,并且在大多数环境下(包括打印),__str__优先于__repr__。如果定义了一个__repr__,打印将会很乐意地调用超类的__str__。

对于未捕获的异常,方法返回的内容都包含在出错消息中,并且打印异常的时候显式化。这里,方法返回一个硬编码的字符串来说明,但是,它也可以执行任意的文本处理,可能附加到实例对象的状态信息。下一小节将介绍状态信息选项。

定制数据和行为

除了支持灵活的层级,异常类还提供了把额外状态信息存储为实例属性的功能。

提供异常细节

当引发一个异常的时候,可能会跨越任意的文件界限——触发异常的raise语句和捕获异常的try语句可能位于完全不同的模块文件中。在一个全局变量中存储额外的细节通常是不可行的,因为try语句可能不知道全局变量位于哪个文件中。在异常自身中传递额外的状态信息,这允许try语句更可靠地访问它。

使用类,这几乎是自动化的。当引发一个异常的时候,Python随着异常传递类实例对象。在try语句中的代码,可以通过在一个except处理器中的as关键字之后列出一个额外的变量,来访问引发的异常。这提供了一个自然的钩子,以用来为处理器提供数据和行为。

例如,解析数据文件的一个程序可能通过引发一个异常实例来表示格式化错误,而该实例用关于错误的额外细节来填充:

class FormatError(Exception):
    def __init__(self,line,file):
        self.line=line
        self.file=file

def parser():
    raise FormatError(42,file='test.txt') # When error found

try:
    parser()
except FormatError as X:
    print('Error at',X.file,X.line)
Error at test.txt 42

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

class FormatError(Exception):pass

def parser():
    raise FormatError(42,'test.txt')
    
try:
    parser()
except FormatError as X:
    print('Error at',X.args[1],X.args[0])
Error at test.txt 42
提供异常方法

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

class FormatError(Exception):
    logfile='formaterror.txt'
    def __init__(self,line,file):
        self.line=line
        self.file=file
    def logerror(self):
        log=open(self.logfile,'a')
        print('Error at',self.file,self.line,file=log)

def parser():
    raise FormatError(40,'test.txt')
    
try:
    parser()
except FormatError as exc:
    exc.logerror()

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

with open('formaterror.txt','r') as f :
    for line in f.readlines():
        print(line)
Error at test.txt 40

在这样的一个类中,方法也可能继承自超类,并且实例属性提供了一个地方来保存状态信息,状态信息提供了额外环境用于随后的方法调用。此外,异常类可以自由地定制和扩展继承的行为。换句话说,由于它们是用类定义的,所以在第六部分中所学习的所有关于OOP的好处,对于Python中的异常来说的都是可用的。

Chap35 异常的设计

嵌套异常处理器

到目前为止,所举的例子都只使用了单一的try语句来捕捉异常,但如果try中还有try,那会发生什么事呢?就此而言,如果try调用一个会执行另一个try的函数,这代表了什么意思呢?从技术角度上讲,从语法和代码运行时的控制流程来看,try语句是可以嵌套的。

如果知道Python会在运行时讲try语句放入堆栈,这两种情况就可以理解了。当发生异常时,Python会回到最近进入、具有相符except分句的try语句。因为每个try语句都会留下标识,Python可检查堆栈的标识,从而跳回较早的try。这种处理器的嵌套化,就是所谈到的异常向上传递至较高的处理器的意思:这类处理器就是在程序执行流程中较早进入的try语句。

进入try代码块的代码量可能很大(例如,它可能包含了函数调用),而且通常会启用正在监视相同异常的其他代码。当异常最终引发时,Python会跳回到匹配该异常、最近进入的try语句,执行该语句的except分句,然后在try语句后继续下去。

一旦异常被捕捉,其生命就结束:控制权不会跳回到所有匹配这个异常、相符的try语句;只有第一个try有机会对它进行处理。与之相对比的是,当try/finally语句嵌套且发生异常时,每个finally代码块都会执行:Python会持续把异常往上传递到其他try语句上,而最终可能达到顶层默认处理器(标准出错消息打印器)。finally子句不会终止异常,而是指明异常传播过程中,离开每个try语句之前要执行的代码。如果异常发生时,有很多try/finally都在活动,它们就都会运行,除非有个try/except在这个过程中捕捉某处该异常。

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

例子:控制流程嵌套

分析一个例子,让这个概念更为具体。以下代码定义了两个函数,action2是写成要触发异常(做数字和序列的加法),而action1把action2调用封装在try处理器内,以捕捉异常。

def action2():
    print(1+[])
def action1():
    try:
        action2()
    except TypeError:
        print('inner try')
try:
    action1()
except TypeError:
    print('outer try')
inner try

可以看到,代码底端的顶层模块代码,也在try处理器中包装了action1调用。当action2触发TypeError异常时,就有两个激活的try语句:其中一个在action1中,另一个在代码底端。Python会挑选并执行具有相符的except、最近的try,而在这个例子中就是action1中的try。

这就正如前边提到的,异常最后所在之处,取决于程序运行时的控制流程。因此,想要知道要去哪里,就需要知道在哪里。就这个例子而言,异常在哪里进行处理是控制流程的函数,而不是语句的语法。然而,也可以用语法把异常处理器嵌套化——等一下会看到与其等效的情况。

例子:语法嵌套化

从语法上有可能让try语句通过其源代码中的位置来实现嵌套。

try:
    try:
        action2()
    except TypeError: # Most recent matching try
        print('inner try')
except TypeError: # Here,only if nested handler re-raises
    print('outer try')

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

try:
    try:
        raise IndexError
    finally:
        print('spam')
finally:
    print('SPAM')
spam
SPAM



---------------------------------------------------------------------------

IndexError                                Traceback (most recent call last)

<ipython-input-18-5eee2ecdf687> in <module>()
      1 try:
      2     try:
----> 3         raise IndexError
      4     finally:
      5         print('spam')


IndexError: 

有关语法嵌套更有有的例子,可以考虑下面的代码

def raise1():raise IndexError
def noraise():return None
def raise2(): raise SyntaxError
    
for func in (raise1,noraise,raiser2):
    print('\n',func,sep='')
    try:
        try:
            func()
        except IndexError:
            print('caught IndexError')
    finally:
        print('finally run')
<function raise1 at 0x000002B07E0AD730>
caught IndexError
finally run

<function noraise at 0x000002B07E0AD158>
finally run

<function raiser2 at 0x000002B07DFBA598>
finally run



---------------------------------------------------------------------------

Specific2                                 Traceback (most recent call last)

<ipython-input-19-061748005958> in <module>()
      7     try:
      8         try:
----> 9             func()
     10         except IndexError:
     11             print('caught IndexError')


<ipython-input-2-772a6fee7a08> in raiser2()
      5 def raiser0():raise General()
      6 def raiser1():raise Specific1()
----> 7 def raiser2():raise Specific2()
      8 
      9 for func in (raiser0,raiser1,raiser2):


Specific2: 

此代码在异常引发时,会对其进行捕捉,而且无论是否发生异常,都会执行finally终止动作。这需要一点时间去理解,但是其效果会很像在单个try语句内结合except和finally。

异常的习惯用法

异常不总是错误

Python中,所有错误都是异常,但不是所有异常都是错误。例如,input函数在每次调用时,都会在文件末尾时引发内置的EOFError。和文件方法不同的是,这个函数并不返回空字符串:input的空字符串是指空行。除了EOFError的名称,这个异常在这种环境下也只是信号而已,不是错误。因为有这种行为,除非文档末尾应该终止脚本,否额,input通常会出现在try处理器内,并嵌入循环内,如下面的代码所示:

while True:
    try:
        line=input()
    except EOFError:
        break
    else:
        ...process next line here...

其他内置异常都是类似的信号,而不是错误——例如,调用sys.exit()并在键盘上按下Ctrl-C,会分别引发SystemExit和KeyboardInterrupt。Python也有一组内置异常,代表警告,而不是错误。其中有些代表了正在使用不推荐的(即将退出的)语言功能的信号。

函数信号条件和raise

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

class Found(Exception):pass

def searcher():
    if ...success...:
        raise Found()
    else:
        return None

try:
    searcher()
except Found:
    ...success...
else:
    ...failure...

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

class Failure(Exception):pass

def searcher():
    if ...success...:
        return ...founditem...
    else:
        raise Failure()
        
try:
    item=searcher()
except Failure:
    ...report...
else:
    ...use item here...

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

关闭文件和服务器连接

作为概括,异常处理工具通常也用来确保系统资源中介,不管在处理过程中是否发生错误。

例如,一些服务器需要关闭连接,从而终止一个会话。与之类似,输出文件可能需要关闭把缓冲区写入磁盘的调用,并且,如果没有关闭输入文件的化,它可能占用文件描述符;尽管在垃圾收集的时候,如果文件对象还打开它会自动关闭,但这有时候很难确保。

确保一个特殊代码块的终止操作的更通用和显式的方式是try/finally语句:

myfile=open(r'C:\misc\script','w')
try:
    ...process myfile...
finally:
    myfile.close()

正如在第33章看到的那样,还可以使用环境管理器,从而自动终止或关闭对象。

而二者选谁,则通常取决于程序。与try/finally相比,环境管理器更为隐式,它与Python通常的设计哲学背道而驰。环境管理器可定也不太常见,它们通常只对选定的对象可用,并且编写用户定义的环境管理器来处理通用的种植需求,比编写要给try/finally更为复杂。

另一方面,使用已有的环境管理器,比使用try/finally需要更少的代码,如前面的例子所示。此外,环境管理器协议除了支持推出动作,还支持进入动作。尽管try/finally可能是更加广为应用的技术,环境管理器可能更适合于可以使用它们的地方,或者可以允许它们的额外复杂性的地方。

在try外进行调试

也可以利用异常处理器,取代Python的默认顶层异常处理行为。在顶层代码中的外层try中包装整个程序(或对它调用),就可以捕捉任何程序执行时会发生的异常,因此可破坏默认的程序终止行为。

下面的代码中,空的except子句会捕捉任何程序执行时所引发的而违背捕捉到的异常。要去的所发生的实际异常,可以从内置sys模块取出sys.exc_info函数的调用结果。这回返回一个u安祖,而元组之前两个元素会自动包含当前异常的类和引发的实例对象(关于sys.exc_info的更多内容稍后介绍)。

try:
    ...run program...
except:
    import sys
    print('uncought!',sys.exc_info()[0],sys.exc_info()[1])

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

运行进程中的测试

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

import sys

log=open('testlog','a')
from testapi import moreTests,runNextTest,testName

def testdrive():
    while moreTests():
        try:
            runNextTest()
        except:
            print('FAILED',testName(),sys.exc_info()[:2],file=log)
        else:
            print('PASSED',testName(),file=log)
testdrive()

这里的testdrive函数会循环进行一组测试调用(在这个例子中,模块testapi是抽象的)。因为测试案例中未被捕捉的异常,一般都会终止这个测试驱动程序,如果想在测试失败后让测试进程继续下区,就需要在try中包装测试案例的调用。就像往常一样,空的except会捕捉由测试案例所产生的没有被捕捉的异常,而其使用sys.exc_info把该异常记录到文件内。没有异常发生时(测试成功情况),else分句就会执行。

对于作为测试驱动运行在同一个进程的函数、模块以及类,而进行测试的系统而言,这种形式固定的代码是很典型的。然而,在实际应用中,测试可能会比这里演示的跟为精致复杂。例如,要测试外部程序时,要改为检查程序启动工具所产生的状态代码或输出,例如,标准库手册哩谈到的os.system和os.popen(这类工具一般不会替外部程序中的错误引发异常。事实上,测试案例可能会和测试驱动并行运行)。

关于sys.exc_info

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

try:
    ...
except:
    # sys.exc_info()[0:2] are the exception class and instance

如果没有处理器正在处理,就返回包含了三个None值的元组。否则,将会返回(type、value和traceback):

  • type是正在处理的异常的异常类型
  • value是引发的异常类实例
  • traceback是一个traceback对象,代表异常最初发生时所调用的堆栈。

当捕获异常分类超类的时候,sys.exc_info对于确定特定的异常类型很有用。由于在这种情况下,也可以通过as子句所获取的实例__class__属性来获得异常类型,sys.exc_info如今主要由空的except使用:

try:
    ...
except General as instance:
    # instance.__class__ is the exception class

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

try:
    ...
except General as instance:
    # instance.method() does the right thing for this instance

通常,在Python中太具体可能会限制代码的灵活性。像这里的最后一个例子的多态方案,通常根号地支持未来的改进。

与异常有关的技巧

应该包装什么

从理论上讲,可以在脚本中把所有的语句都包装在try中,但这样做不够明治。这其实是设计的问题,不在语言本身的范围内,而实际运用时,就会更明显。以下时一些简要的原则。

  • 经常会失败的运算一般都应该包装在try语句内。例如,和系统状态衔接的运算(文件开启、套接字调用等)就是try的主要候选者。
  • 但上边的规则也有些特例:在简单的脚本中,会希望这类运算失败时终止程序,而不是被捕捉或是被忽略。如果是一个重大的错误,更是如此。Python中的错误会产生有用的出错消息(如果不是崩溃的话),而且这通常就是所期望的最好结果。
  • 应该在try/finally中实现终止动作,从而保证它们的执行,除非环境管理器作为一个with/as选项可用。这个语句的形式可以执行代码,无论异常是否发生。
  • 偶尔,把对大型函数的调用包装在单个try语句内,而不是让函数本身零散着放入若干try语句中,这样会更方便。这样的话,函数中的异常就会往上传递到调用周围的try,而程序员也可以减少函数中的代码量。

捕捉太多:避免空except的语句

另一个问题是处理器的通用性问题。Python可选择哟啊捕捉哪些异常,有时候必须小心,不要涵盖太广。

空except很容易编写,有时候也是想要的结果,但是可能拦截到异常嵌套结构中较高层的try处理器所期待的事件。例如,下列的异常处理器,会捕捉每个到达的异常并使其停止,无论是否有另一个处理器在等待该事件。

def func():
    try:
        ... # IndexError is raised in here
    except:
        ... # But everything comes here and dies!
try:
    func()
except IndexError: # Exception should be precessed here
    ...

也许更糟,这类代码可能会捕捉无关的系统异常。而这类异常通常是不应该拦截的。

例如,当控制权到达顶层文件末尾时,脚本正常时退出的。然而,Python也提供内置sys.exit(statuscode)调用来提前终止。这实际上是引发内置的SystemExit异常来终止程序,使tyr/finally可以在离开前执行,而程序的特殊类型可拦截该事件。因此,try带空except时,可能会不知不觉阻止重要的结束。如下面文件所示:

import sys

def bye():
    sys.exit(40) # Crucial error :abort now!
try:
    bye()
except:
    print('got it') # Oops--we ignored the exit
print('contining')
got it
contining

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

try:
    bye()
except Exception: # Won't catch exits,but will catch many others
    ...

在其他情况下,这种方案并不比空的except子句好——因为Exception是所有内置异常(除了系统退出事件以外)之上的一个超类,它仍然有潜力捕获程序中其他地方的异常。

最糟的情况是,空except和捕获Exception类也会捕捉一般程序设计错误,但这类错误多数时候都应让其通过。事实上,这两种技术都会有效关闭Python的错误报告机制,使得代码中的错误难以发现。例如,考虑下面的代码:

mydictionary={...}
...
try:
    x=myditctionary['spam'] # Oops:misspelled
except:
    x=None  # Assume we got KeyError
...continue here with x...

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

经验法则是,尽量让处理器具体化:空except子句很方便,但是可能容易出错。

捕捉过少:使用基于类的分类

另一方面,处理器也不应过于具体化。当在try中列出特定的异常时,就只捕捉实际所列出的事件。这不见得是坏事,如果系统演进发展,以后会引发其他的异常,就得回头在代码其他地方,把这些新的异常加入异常的列表。

例如,下面的处理器把MyExcept1和MyExcept2看作是正常的情况,而把其他一切视为错误。但若将来增加了MyExcept3,就会被视为错误并处理,除非更新异常列表:

try:
    ...
execpt (MyExcept1,MyExcept2): # Breaks if add a MyExcept3
    ... # Non-errors
else:
    ... # Assumed to be an error

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

try:
    ...
except SuccessCategoryNme: # Ok if add a myerror3 subclass
    ... # Non-errors
else:
    ... # Assumed to be an error

无论是否使用基于类的异常的分类层次,采用一点细微的设计,就可以走得长远。这个故事的寓意是:异常处理器不要过于一般化,也不要太过于具体化,而且要明智选择try语句所包装的代码量。特别是在较大系统中,异常规则也应该是整体设计的一部分。

核心语言总结

这是对Python这个语言整个核心部分的一个回顾!

当你看到这的话,将来在简历上就可以自信地加上Python了!

Python工具集

一般而言,Python提供了一个有层次的工具集。

  • 内置工具:像字符串、列表以及字典这些内置类型,会让编写简单的程序更为迅速。
  • Python扩展:更重要的任务来说,可以编写自己的函数、模块以及类来扩展Python
  • 已编译的扩展:虽然这里不会介绍这一话题,但Python也可以使用C或C++这样的外部语言所编写的模块来进行扩展。

因为Python讲其工具集分层,可以决定程序任务要多么的深入这个层次:可以让简单脚本使用内置工具,替较大系统新增Python所编写的扩展工具,并且为高级工作编写那些编译好的扩展工具。

下表总结了Python程序员可用的内置或现有的功能来源,而有些话题可能会用剩余的Python生涯事件来探索。到目前为止,这里所举例的例子都很小、独立完备。它们是有意这样编写的,也就是为了能够帮助学习者掌握基础知识。但既然了解核心语言知识的,就该是学习如何使用Python内置接口进行实际工作的时候了。你会发现,利用Python这种简单的语言,常见任务会比想象的更为简单。

Python的工具箱类型

分类例子
对象类型列表、字典、文件和字符串
函数len、range、open
异常IndexError、KeyError
模块os、tkinter、pickle、re
属性__dict__、__name__、__class__
外部工具NumPy、SWIG、Jython、IronPython、Django等

大型项目的开发工具

一旦精通了Python基础知识,就会发现Python程序变得比你至今体验过的例子还要大。对于开发大型系统而言,Python和公开领域有一批开发工具可以使用。你已经看过其中几种工具的用法,而且本书也提到过一些。以下是此领域中一些最常用的工具的摘要。

PyDoc和文档字符串

PyDoc的help函数和HTML界面在第15章介绍过。PyDoc为模块和对象提供了一个文档系统,并整合了Python的文档字符串功能。这是Python系统的标准部分,参考库手册以获得更多细节。

PyChecker和Pylint

因为Python是一门动态语言,所以有些程序设计的错误在程序运行前不会报错(例如,当文件执行或导入时,语法错误会被捕捉)。这不是什么大的缺点:就大多数语言一样,这知识代表把产品传送给客户钱,需要测试你的Python程序。此外,Python的动态本质、自动出错消息以及异常模型,使你很容易在Python中寻找和修改错误,远胜过其他语言。

PyChecker和PyLint系统可以在脚本运行前把大量的常见错误预先缓存起来。这和C开发领域中的"lint"程序扮演了类似的角色。有些Python社区会在测试或者分发给客户前,先用PyCHecker执行其代码,来捕捉任何潜在的问题。事实上,Python标准库在发布前都会定期用PyChecker执行。PyChecker和Pylint是第三方开源代码包。可以在http://www.python.org或者PyPI网站上找到它们。

PyUnit

Python有两个测试辅助工具。第一个是PyUnit(在库手册中称为unittest),提供了一个面向对象的类框架,来定义和定制测试案例以及预期的结果。这是模拟JAVA的JUnit框架的。这是个精致的类系统。

doctest

doctest标准库模块,提供第二个并且更为简单的做法来进行回归测试。这是基于Python的文档字符串功能的。概括地讲,要使用doctest时,把交互模式测试会话的记录复制粘贴到源代码文件中的文档字符串中。然后,doctest回抽取出你的文档字符串,分解出测试案例和结果,然后重新执行测试,并验证预期的结果。doctest的操作可以用多种方式剪裁。

IDE

例如IDLE这类IDE,提供了图形环境,来编辑、运行、调试以及浏览Python程序。有些高级的IDE支持其他的开发任务,包括源代码控制整合、GUI交互构建工具和项目文件等。

配置工具

因为Python是高级和动态的,所以从跟其他语言学习得到的之间经验,通常不适用于Python代码。为了真正把代码中的性能瓶颈隔离出来,需要通过time或timeit模块内的时钟工具新增计时逻辑,或者在profile模块下运行代码。

profile是标准库模块,为Python实现源代码配置工具。它会运行你所提供的代码的字符串(例如,脚本文件的导入、或者对函数的调用)。在默认情况下,它会打印报告到标准输出流,从而给出性能的统计结果:每个函数的调用次数、每个函数所化事件等。

profile模块可以作为一个脚本运行或导入,并且可以以多种方式进行定制。例如,可以把统计资料保存到文件中,稍后使用pstats模块进行分析。要交互地进行配置,导入profile模块并调用profile.run(‘code’),把想要配置的diamagnetic作为一个字符串传入(例如,对函数的一次调用或者对整个文件的导入)。要从一个系统shell命令行配置,使用一条形式为python -m profile main.py args的命令。参阅Python标准库手册来了解其他的配置选项:例如,cProfile模块,用于与profile相同的接口,但是运行起来负载较少,因此它可能适合于配置长时间运行的程序。

调试器

作为一个回顾,Python的大多数开发IDE都支持基于GUI的调试,并且Python标准库包含了一个命令行源代码调试器模块,称为pdb。这个模块提供了与常用的C语言的调试器工作非常类似的一个命令行界面。

和配置器很相似,pdb调试器可以交互地运行,或者从一个命令行运行,并且可以从一个Python程序导入并调用。要交互地使用它,导入这个模块,调用pdb函数开始执行代码(例如,pdb.run(“main()”)),然后再交互模式提示符下输入调试命令。要从一个系统shell命令行启动pdb,使用形式为python -m pdb main.py args…的一条命令。pdb包括了实用的事后分析调用,即pdb.pm(),它可在遇到异常后启动调试器。

因为IDLE这类IDE包括“指针并点击”的调试界面,pdb其实现在很少有人使用。

发布的选择

py2exe、PyInstaller以及freeze都可以打包字节码以及Python虚拟机,从而称为“冻结二进制”的独立的可执行文件也就是不需要目标机器上有安装Python,完全可以隐藏系统的代码。此外,当Python程序分发个客户时,可以采用源代码的形式或字节码的形式(.pyc),此外,导入钩子支持特殊的打包技术,例如,zip文件自动解压缩以及字节码加密。distutils模块,为Python模块和套件以及C编写而成的扩展工具提供了各种打包选项,更多细节参考Python手册。后起之秀Python eggs打包系统提供另一种做法,也可解决包的相互依赖性,更多细节请在互联网上搜索。

优化选项

优化程序时,Psyco系统提供了实时的编译器,可以把Python字节码翻译成二进制机码,而Shedskin提供了Python对C++的翻译器。偶尔会看见.pyo优化后的字节码文件,这是以-o python命令标志位运行所产生的文件,这只提供了优先的性能提升,并不常用。最后,也可以把程序的一部分改写为用C这类变异性语言编写,从而提高程序性能。一般来说,Python的速度也会随着事件不断改善,要尽可能升级到最新的版本。

对于大型项目的提示

在本书中遇到过各种语言特性,当开始编写大型项目时,就会变得更有用。其中,模块包(23章)、基于类的异常(33章)、类的伪私有属性(30章)、文档字符串(15章)、模块路径配置文件(21章从from *以及__all__列表到_X风格的变量名来隐藏变量名(24章)、以__name__==’__main__'技巧来增加自我测试代码(24章)和使用函数和模块的常见设计规则(17、19、24章),使用面向对象设计模式(30章及其他),等等。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值