• 注册
当前位置:1313e > python >正文

python浓缩(10)

为什么80%的码农都做不了架构师?>>>   hot3.png

1 . 从 Python 1.5 开始, 所有的标准异常都使用类来实现.

本章将介绍什么是异常, 异常处理, 以及 Python 对异常的支持. 我们还会介绍如何在代码里生成异常. 最后, 我们会涉及如何创建自定义的异常类.

10.1 什么是异常

10.1.1 错误

在深入介绍异常之前, 我们来看看什么是错误. 从软件方面来说, 错误是语法或是逻辑上的.

  • 语法错误指示软件的结构上有错误, 导致不能被解释器解释或编译器无法编译. 这些错误必须在程序执行前纠正.

  • 当程序的语法正确后, 剩下的就是逻辑错误了. 逻辑错误可能是由于不完整或是不合法的输入所致;

  • 在其他情况下, 还可能是逻辑无法生成, 计算, 或是输出结果需要的过程无法执行.

这些错误通常分别被称为域错误和范围错误.

当 Python 检测到一个错误时, 解释器就会指出当前流已经无法继续执行下去. 这时候就出现了异常.

10.1.2 异常

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

程序运行时发生的错误主要是由于外部原因引起的, 例如非法输入或是其他操作失败等等. 这些因素并不在程序员的直接控制下, 而程序员只能预见一部分错误,编写常见的补救措施代码.

类似 Python 这样支持引发和处理异常(这更重要)的语言, 可以让开发人员可以在错误发生时更直接地控制它们. 程序员不仅仅有了检测错误的能力, 还可以在它们发生时采取更可靠的补救措施. 

我们可以为不同的异常创建不同的处理器, 而不是盲目地创建一个"catch-all/捕获所有"的代码.

10.2 Python 中的异常

在先前的一些章节里你已经执行了一些代码, 你一定遇到了程序"崩溃"或因未解决的错误而终止的情况. 你会看到"traceback/跟踪返回"消息, 以及随后解释器向你提供的信息, 包括错误的名称, 原因, 以及发生错误的行号.

(1)NameError: 尝试访问一个未申明的变量

>>> foo
Traceback (innermost last): File "", line 1, in ?
NameError: name 'foo' is not defined

NameError 表示我们访问了一个没有初始化的变量.  我们将在后面的两章讨论名称空间, 现在大家可以认为它们是连接名字和对象的"地址簿"就可以了. 任何可访问的变量必须在名称空间里列出. 访问变量需要由解释器进行搜索,如果请求的名字没有在任何名称空间里找到, 那么将会生成一个 NameError 异常.

(2)ZeroDivisionError: 除数为零

>>> 1/0
Traceback (innermost last): File "", line 1, in ?
ZeroDivisionError: integer division or modulo by zero

我们边的例子使用的是整数, 但事实上, 任何数值被零除都会导致一个 ZeroDivisionError异常.

(3)SyntaxError: Python 解释器语法错误

>>> for
File "", line 1
Edit By Vheavens
Edit By Vheavens
for
^
SyntaxError: invalid syntax

SyntaxError 异常是唯一不是在运行时发生的异常. 它代表 Python 代码中有一个不正确的结构, 在它改正之前程序无法执行. 这些错误一般都是在编译时发生, Python 解释器无法把你的脚本转化为 Python 字节代码. 当然这也可能是你导入一个有缺陷的模块的时候.

(4)IndexError:请求的索引超出序列范围

>>> aList = []
>>> aList[0]
Traceback (innermost last): File "", line 1, in ?
IndexError: list index out of range

IndexError 在你尝试使用一个超出范围的值索引序列时引发.

(5)KeyError:请求一个不存在的字典关键字

>>> aDict = {'host': 'earth', 'port': 80}
>>> print aDict['server'] Traceback (innermost last):
File "", line 1, in ? KeyError: server

映射对象, 例如字典, 是依靠关键字(keys)访问数据值的. 如果使用错误的或是不存在的键请求字典就会引发一个 KeyError 异常.

(6)IOError: 输入/输出错误

>>> f = open("blah") Traceback (innermost last):
File "", line 1, in ?
IOError: [Errno 2] No such file or directory: 'blah'

类似尝试打开一个不存在的磁盘文件一类的操作会引发一个操作系统输入/输出(I/O)错误. 任何类型的 I/O 错误都会引发 IOError 异常.

(7)AttributeError: 尝试访问未知的对象属性

>>> class myClass(object):
... pass
...
>>> myInst = myClass()
>>> myInst.bar = 'spam'
>>> myInst.bar
'spam'
>>> myInst.foo
Traceback (innermost last): File "", line 1, in ?
AttributeError: foo

10.3 检测和处理异常

异常可以通过 try 语句来检测. 任何在 try 语句块里的代码都会被监测, 检查有无异常发生.

try 语句有两种主要形式: try-except 和 try-finally . 这两个语句是互斥的, 也就是说你只能使用其中的一种. 一个 try 语句可以对应一个或多个 except 子句, 但只能对应一个finally 子句, 或是一个 try-except-finally 复合语句. 你也可以添加一个可选的 else 子句处理没有探测到异常的时执行的代码. 而 try-finally 只允许检测异常并做一些必要的清除工作(无论发生错误与否), 没有任何异常处理设施. 正如你想像的,复合语句两者都可以做到.

10.3.1 try-except 语句

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

try:
try_suite # watch for exceptions here 监控这里的异常
except Exception[, reason]:
except_suite # exception-handling code 异常处理代码
>>> try:
...     f = open('blah', 'r')
... except IOError, e:
...     print 'could not open file:', e
...
could not open file: [Errno 2] No such file or directory

我们在尝试打开一个不存在的文件时仍然发生了 IOError . 有什么区别么? 我们加入了探测和错误错误的代码. 当引发 IOError异常时, 我们告诉解释器让它打印出一条诊断信息. 程序继续执行, 而不像以前的例子那样被"轰

出来" - 异常处理小小地显了下身手. 那么在代码方面发生了什么呢?

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

在我们上边的例子中, 我们只捕获 IOError 异常. 任何其他异常不会被我们指定的处理器捕获. 举例说, 如果你要捕获一个 OSError , 你必须加入一个特定的异常处理器. 我们将在本章后面详细地介绍 try-except 语法.

核心笔记: 忽略代码, 继续执行, 和向上移交

try 语句块中异常发生点后的剩余语句永远不会到达(所以也永远不会执行). 一旦一个异常被引发, 就必须决定控制流下一步到达的位置. 剩余代码将被忽略, 解释器将搜索处理器, 一旦找到,就开始执行处理器中的代码.如果没有找到合适的处理器, 那么异常就向上移交给调用者去处理, 这意味着堆栈框架立即回到之前的那个. 如果在上层调用者也没找到对应处理器, 该异常会继续被向上移交, 直到找到合适处理器. 如果到达最顶层仍然没有找到对应处理器, 那么就认为这个异常是未处理的, Python 解释器会显示出跟踪返回消息, 然后退出.

10.3.2 封装内建函数

从最基本的错误检测开始, 然后逐步改进它, 增强代码的健壮性. 这里的问ti是把一个用字符串表示的数值转换为正确的数值表示形式, 而且在过程中要检测并处理可能的错误.

float() 内建函数的基本作用是把任意一个数值类型转换为一个浮点数. 从 Python 1.5 开始,float() 增加了把字符串表示的数值转换为浮点数的功能, 没必要使用 string 模块中的 atof()函数. 如果你使用的老版本的 Python , 请使用 string.atof() 替换这里的 float() .

>>> float(12345)
12345.0
>>> float('12345')
12345.0
>>> float('123.45e67')
1.2345e+069

不幸的是, float() 对输入很挑剔:

>>> float('foo')
Traceback (innermost last): File "", line 1, in ?
float('foo')
ValueError: invalid literal for float(): foo
>>>
>>> float(['this is', 1, 'list']) Traceback (innermost last):
File "", line 1, in ?
float(['this is', 1, 'list'])
TypeError: float() argument must be a string or a number

从上面的错误我们可以看出, float() 对不合法的参数很不客气. 例如, 如果参数的类型正确(字符串), 但值不可转换为浮点数, 那么将引发 ValueError 异常, 因为这是值的错误. 列表也是不合法的参数, 因为他的类型不正确, 所以, 引发一个 TypeError 异常.

我们的目标是"安全地"调用 float() 函数, 或是使用一个"安全的方式" 忽略掉错误, 因为它们与我们转换数值类型的目标没有任何联系, 而且这些错误也没有严重到要让解释器终止执行. 在 try-except 的协助下创建我们预想的环境, 我们把他叫做 safe_float() . 在第一次改进中我们搜索并忽略 ValueError , 因为这是最常发生的. 而 TypeError 并不常见, 我们一般不会把非字符串数据传递给 float().

def safe_float(obj):try:return float(obj)except ValueError:pass

错误会被探测到, 而我们在 except 从句里没有放任何东西,不进行任何处理, 忽略这个错误.

这个解决方法有一个明显的不足, 它在出现错误的时候没有明确地返回任何信息. 虽然返回了None, 我们并没有得到任何关于出错信息的提示. 我们至少应该显式地返回 None , 来使代码更容易理解:

def safe_float(obj):try:retval = float(obj)except ValueError:retval = Nonereturn retval

你可以在文档中这样写, 如果传递给 safe_float() 合适的参数, 它将返回一个浮点数; 如果出现错误, 将返回一个字符串. 我们按照这个方案再修改一次代码, 如下所示:

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

我们有了一个好的开始 - 现在我们已经可以探测到非法的字符串输入了, 可如果传递的是一个非法的对象, 还是会"受伤":

在进一步改进程序之前, 首先来看看 try-except 的其他灵活的语法, 特别是 except 语句, 它有好几种变化形式.

10.3.3 带有多个 except 的 try 语句

这种格式的except语句指定检测名为 Exception 的异常. 你可以把多个 except 语句连接在一起, 处理一个 try 块中可能发生的多种异常, 如下所示:

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

我们的 safe_float() 函数已经可以检测到指定的异常了. 更聪明的代码能够处理好每一种异常. 

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

10.3.4 处理多个异常的 except 语句

我们还可以在一个 except 子句里处理多个异常. except 语句在处理多个异常时要求异常被放在一个元组里:

except (Exception1, Exception2)[, reason]:suite_for_Exception1_and_Exception2

上边的语法展示了如何处理同时处理两个异常. 事实上 except 语句可以处理任意多个异常,前提只是它们被放入一个元组里 , 如下所示:

except (Exc1[, Exc2[, ... ExcN]])[, reason]:suite_for_exceptions_Exc1_to_ExcN

如果由于其他原因, 也许是内存规定或是设计方面的因素, 要求 safe_float() 函数中的所有异常必须使用同样的代码处理, 那么我们可以这样满足需求:

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

现在, 错误的输入会返回相同的字符串:

>>> safe_float('Spanish Inquisition')
'argument must be a number or numeric string'
>>> safe_float([])
'argument must be a number or numeric string'
>>> safe_float(932)
932.0

10.3.5 捕获所有异常

如果我们想要捕获所有的异常呢? 当然可以! 异常成为类,我们现在有一个异常继承结构可以遵循.如果查询异常继承的树结构, 我们会发现 Exception 是在最顶层的:

try:
except Exception, e:

另一个我们不太推荐的方法是使用 裸except 子句:

try:
except:

这个语法不如前个 "Pythonic" . 虽然这样的代码捕获大多异常, 但它不是好的 Python 编程样式. 一个主要原因是它不会kao虑潜在的会导致异常的主要原因. 我们的 catch-all 语句可能不会如你所想的那样工作, 它不会调查发生了什么样的错误, 如何避免它们.我们没有指定任何要捕获的异常 - 这不会给我们任何关于可能发生的错误的信息. 另外它会捕获所有异常, 你可能会忽略掉重要的错误, 正常情况下这些错误应该让调用者知道并做一定处理.

最后, 我们没有机会保存异常发生的原因. 当然, 你可以通过sys.exc_info() 获得它, 但这样你就不得不去导入 sys 模块, 然后执行函数 - 这样的操作本来是可以避免的, 尤其当我们需要立即告诉用户为什么发生异常的时候.在Python 的未来版本中很可能不再支持裸except 子句.

关于捕获所有异常, 你应当知道有些异常不是由于错误条件引起的. 它们是 SystemExit 和KeyboardInterupt . SystemExit 是由于当前 Python 应用程序需要退出, KeyboardInterupt 代表用户按下了 CTRL-C (^C) , 想要关闭 Python . 在真正需要的时候, 这些异常却会被异常处理捕获.

一个典型的迂回工作法代码框架可能会是这样:

try:
except (KeyboardInterupt, SystemExit):# user wants to quit
raise# reraise back to caller
except Exception:# handle real errors

异常被迁移到了 new-style class 上,启用了一个新的"所有异常的母亲", 这个类叫做 BaseException , 异常的继承结构有了少许调整,

为了让人们摆脱不得不除创建两个处理器的惯用法. KeyboardInterrupt 和 SystemExit 被从Exception 里移出, 和 Exception 平级:

- BaseException

|- KeyboardInterrupt

|- SystemExit

|- Exception

|- (all other current built-in exceptions) 所有当前内建异常

你可以在表 10.2 找到整个异常继承结构(变化前后).

这样, 当你已经有了一个 Exception 处理器后, 你不必为这两个异常创建额外的处理器. 代码将会是这样:

try:
except Exception, e:# handle real errors

如果你确实需要捕获所有异常, 那么你就得使用新的 BaseException :

try:
except BaseException, e:# handle all errors

当然, 也可以使用不被推荐的裸except 语句.

核心风格: 不要处理并忽略所有错误

它的目的是减少程序出错的次数并在出错后仍能保证程序正常执行. 一个不正确的使用方法就是把它作为一个大绷带"绑定"到一大片代码上. 也就是说把一大段程序(如果还不是整个程序源代码的话)放入一个 try 块中, 再用一个通用的 except 语句 "过滤"掉任何致命的错误, 忽略它们.

# this is really bad code
try:large_block_of_code # bandage of large piece of code
except Exception: # same as except:pass # blind eye ignoring all errors

很明显, 错误无法避免, try-except 的作用是提供一个可以提示错误或处理错误的机制, 而不是一个错误过滤器.

你可以捕获特定的异常并忽略它们, 或是捕获所有异常并采取特定的动作. 不要捕获所有异常,然后忽略掉它们.

10.3.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 类的实例.

对于大多内建异常, 也就是从 StandardError 派生的异常, 这个元组只包含一个指示错误原因的字符串. 一般说来, 异常的名字已经是一个满意的线索了, 但这个错误字符串会提供更多的信息. 

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

核心风格: 遵循异常参数规范

当你在自己的代码中引发内建(built-in)的异常时, 尽量遵循规范, 用和已有 Python 代码一致错误信息作为传给异常的参数元组的一部分. 简单地说, 如果你引发一个 ValueError , 那么

最好提供和解释器引发 ValueError 时一致的参数信息, 如此类推. 这样可以在保证代码一致性,同时也能避免其他应用程序在使用你的模块时发生错误.

如下边的例子, 它传参给内建 float 函数一个无效的对象, 引发TypeError 异常:

>>> try:
...     float(['float() does not', 'like lists', 2])
... except TypeError, diag:# capture diagnostic info
...     pass
...
>>> type(diag)

>>>
>>> print diag
float() argument must be a string or a number

我们首先在一个try 语句块中引发一个异常,随后简单的忽略了这个异常,但保留了错误的信息。调用内置的type()函数,我们可以确认我们的异常对象的确是TypeError 异常类的实例。最后我们对异常诊断参数调用print 以显示错误。

为了获得更多的关于异常的信息,我们可以调用该实例的 __class__ 属性,它标示了实例是从什么类实例化而来. 类对象也有属性, 比如文档字符串(documentation string)和进一步阐明错误类型的名称字符串:

>>> diag # exception instance object
>>> diag.__class__ # exception class object
>>> diag.__class__.__doc__ # exception class documentation string'Inappropriate argument type.'
>>> diag.__class__.__name__ # exception class name'TypeError'

我们会在第13 章"类和面向对象编程"发现, __class__ 属性存在于所有的类实例中,而__doc__类属性存在于所有的定义了文档字符串的类中.

我们现在再次来改进我们的saft_float()以包含异常参数,当float()发生异常时传给解释器.在前一次改进中,我们在一句话中同时捕获了ValueError 和TypeError 异常以满足某些需求.但还是有瑕疵,那个解决方案中没有线索表明是哪一种异常引发了错误.它仅仅是返回了一个错误字符串指出有无效的参数.现在,通过异常参数,可以改善这种状况.

因为每一个异常都将生成自己的异常参数,如果我们选择用这个字符串来而不是我们自定义的信息, 可以提供一个更好的线索来指出问ti. 下面的代码片段中, 我们用字符串化(string representation)的异常参数来替换单一的错误信息.

def safe_float(object):try:retval = float(object)except (ValueError, TypeError), diag:retval = str(diag)return retval
>>> safe_float('xyz')
'invalid literal for float(): xyz'
>>> safe_float({})
'object can't be converted to float'
Edit By Vheavens
Edit By Vheavens

10.3.7 在应用使用我们封装的函数

打开信用卡交易的数据文件(carddata.txt),加载所有的交易,包括解释的字符串.下面是一个示例的carddate.txt 文件:

# carddata.txt previous balance
25
debits
21.64
541.24
25
credits
-25
-541.24
finance charge/late fees
7.30
5

示例 10.1 信用卡交易系统(cardrun.py) 我们用safe_float()来处理信用卡交易文件,将其作为字符串读入.并用一个日志文件跟踪处理进程.

def safe_float(obj):'safe version of float()'try:retval = float(obj)except(ValueError, TypeError) as diag:retval = str(diag)return retvaldef main():'handles all the data processing'log = open('cardlog.txt','w')try:cfile = open('carddata.txt', 'r')except IOError as e:log.write('no txns this month\n')log.close()returntxns = cfile.readlines()cfile.close()total = 0.00log.write("account log: \n")for eachTxn in txns:result = safe_float(eachTxn)if isinstance(result,float):total+= resultelse:log.write('ignored', result)print ('$%.2f (new balance)' % (total))log.close()
if __name__=='__main__':main()

10.3.8 else 子句

至于try-except 语句段,它的功能和你所见过的其他else 没有太多的不同:在try 范围中没有异常被检测到时,执行else 子句.下面是用Python 伪代码写的简短例子.

import 3rd_party_module
log = open('logfile.txt', 'w')
try:3rd_party_module.function()
except:log.write("*** caught exception in module\n")
else:log.write("*** no exceptions caught\n")log.close()

在前面的例子中,我们导入了一个外部的模块然后测试是否有错误.用一个日志文件来确定这个第三方模块是有无缺陷.根据运行时是否引发异常,我们将在日志中写入不同的消息.

10.3.9 finally 子句

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

下面是try-except-else-finally 语法的示例:

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

当然,无论如何,你都可以有不止一个的except 子句,但最少有一个except 语句,而else 和finally 都是可选的.A,B,C 和D 是程序(代码块).程序会按预期的顺序执行.(注意:可能的顺序是A-C-D[正常]或A-B-D[异常]).

10.3.10 try-finally 语句

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

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

当在try范围中产生一个异常时,会立即跳转到finally 语句段.当finally 中的所有代码都执行完毕后,会继续向上一层引发异常.因而常常看到嵌套在try-except 中的try-finally 语句.

当在读取carddata.txt 中文本时可能引发异常,我们可以在cardrun.py 的这一处添加try-finally 语句段来改进代码.在当前示例10.1的代码中,我们在读取阶段没有探测到错误(通过readlines())

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

但有很多原因会导致readlines()失败,其中一种就是carddata.txt 存在于网络(或软盘)上,但是变得不能读取.无论怎样,我们可以把这一小段读取数据的代码整个放入try 子句的范围中:

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

我们所做的一切不过是将readline()和close()方法调用都移入了try 语句段.尽管我们代码变得更加的健壮了,但还有改进的空间.注意如果按照这样的顺序发生错误:打开成功,但是出于一些原因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-finally 语句段实现).另一种可选的实现切换了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 会自动的重新引发异常.

一个这样写的理由是如果在finally 的语句块内发生了一个异常,你可以创建一个同现有的异常处理器在同一个(外)层次的异常处理器来处理它.这样,从本质上来说,就可以同时处理在原始的try语句块和finally 语句块中发生的错误.这种方法唯一的问ti是,当finally 语句块中的确发生异常时,你会丢失原来异常的上下文信息,除非你在某个地方保存了它.

反对这种写法的一个理由是:在很多情况下,异常处理器需要做一些扫尾工作,而如果你在异常处理之前,用finally 语句块中释放了某些资源,你就不能再去做这项工作了.简单的说,finally 语句块并不是如你所想的是"最终的(final)"了.

一个最终的注意点:如果finally 中的代码引发了另一个异常或由于return,break,continue 语法而终止,原来的异常将丢失而且无法重新引发.

10.3.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

这一节最重要的是无论你选择什么语法,你至少要有一个except 子句,而else 和finally 都是可选的.

10.4 上下文管理

10.4.1 with 语句

另一个隐藏低层次的抽象的例子是with 语句,(Python2.5 尝试性的引入了with, 但必须用from __future__ importwith_statement 来导入它.)

类似try-except-finally , with 语句也是用来简化代码的.try-except 和try-finally 的一种特定的配合用法是保证共享的资源的唯一分配,并在任务结束的时候释放它.比如文件(数据,日志,数据库等等),线程资源,简单同步,数

据库连接,等等. with 语句的目标就是应用在这种场景.

然而,with 语句的目的在于从流程图中把 try,except 和finally 关键字和资源分配释放相关代码统统去掉,with 语法如下:

with context_expr [as var]:with_suite

这并不如看上去的那么容易,因为你不能对Python 的任意符号使用with 语句.它仅能工作于支持上下文管理协议(context managementprotocol)的对象.这显然意味着只有内建了"上下文管理"的对象可以和with 一起工作.我们过一会再来阐明它的含义.

目前已经有了一些支持该协议的对象.下面是第一批成员的简短列表:

? file

? decimal.Context

? thread.LockType

? threading.Lock

? threading.RLock

? threading.Condition

? threading.Semaphore

? threading.BoundedSemaphore

下面就给出一段file和with 一起使用的代码片段.

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

它会完成准备工作,比如试图打开一个文件,如果一切正常,把文件对象赋值给f.然后用迭代器遍历文件中的每一行,当完成时,关闭文件.无论的在这一段代码的开始,中间,还是结束时发生异常,会执行清理的代码,此外文件仍会被自动的关闭.

因为已经从你手边拿走了一堆细节,所以实际上只是进行了两层处理:

  • 第一,发生用户层 —— 和 in 类似,你所需要关心的只是被使用的对象

  • 第二,在对象层.既然这个对象支持上下文管理协议,它干的也就是"上下文管理".

10.4.2 *上下文管理协议

除非你打算自定义可以和with 一起工作的类,比如:别的程序员会在他们的设计的应用中使用你的对象.

我们不打算在这里对上下文管理做深入且详细的探讨,但会介绍兼容协议所必须的对象类型与功能,使其能和with 一起工作.

前面,我们在例子中描述了一些关于协议如何和文件对象协同工作.让我们在此进一步地研究.

上下文表达式(context_expr),上下文管理器

当with 语句执行时,便执行上下文符号(译者注:就是with 与as 间内容)来获得一个上下文管理器.上下文管理器的职责是提供一个上下文对象.这是通过调用__context__()方法来实现的.该方法返回一个上下文对象,用于在with 语句块中处理细节.有点需要注意的是上下文对象本身就可以是上下文管理器.所以context_expr 既可以为一个真正的上下文管理器,也可以是一个可以自我管理的上下文对象.在后一种情况时,上下文对象仍然有__context__()方法,返回其自身,如你所想.上下文对象,with 语句块一旦我们获得了上下文对象,就会调用它的__enter()__方法.它将完成with 语句块执行前的所有准备工作.你可以注意到在上面的with 行的语法中有一个可选的as 声明变量跟随在context_expr之后.如果提供提供了变量,以__enter()__返回的内容来赋值;否则,丢弃返回值.在我们的文件对象例子中,上下文对象的__enter()__返回文件对象并赋值给f.现在,执行了with 语句块.当with 语句块执行结束,无论是"和谐地"还是由于异常,都会调用上下文对象的__exit()__方法.__exit__()有三个参数.如果with 语句块正常结束,三个参数全部是None.如果发生异常,三个参数的值的分别等于调用sys.exc_info()函数(见10.12)返回的三个值:类型(异常类),值(异常实例),和回溯(traceback),相应的回溯对象.你可以自己决定如何在__exit__()里面处理异常.惯例是当你处理完异常时不返回任何值,或返回None,或返回其他布尔值为False 对象.这样可以使异常抛给你的用户来处理.如果你明确的想屏蔽这个异常,返回一个布尔为True 的值.如果没有发生异常或你在处理异常后返回True,程序会继续执行with 子句后的下一段代码.因为上下文管理器主要作用于共享资源,你可以想象到__enter()__和__exit()__方法基本是干的需要分配和释放资源的低层次工作,比如:数据库连接,锁分配,信号量加减,状态管理,打开/关闭文件,异常处理,等等.

为了帮助你编写对象的上下文管理器, 有一个contextlib 模块, 包含了实用的functions/decorators, 你可以用在你的函数/ 对象上而不用去操心关于类或__context__(),__enter()__,__enter()__,__exit()__这些方法的实现.想了解更多关于上下文管理器的信息,查看官方的Python 文档的with 语法和contextlib 模块,类的指定方法(与with 和contexts 相关的),PEP 343,和“What’s New in Python 2.5(Python 2.5的更新)”的文档.

10.5 *字符串作为异常

(1)早在Python 1.5 前,标准的异常是基于字符串实现的.然而,这样就限制了异常之间不能有相互的关系.

(2)到1.5 为止,所有的标准异常都是类了.程序员还是可以用字符串作为自己的异常的,但是我们建议从现在起使用异常类.

(3)为了向后兼容性,还是可以启用基于字符串的异常.从命令行以-X 为参数启动Python 可以提供你字符串方式的标准异常.从Python1.6 起这个特性被视为废弃的.

(4)Python 2.5 开始处理向来不赞成使用的字符串异常.在2.5 中,触发字符串异常会导致一个警告.

(5)在2.6,捕获字符串异常会导致一个警告.由于它很少被使用而且已经被废弃,我们将不再在本书范围内讨论并且已经去除相关文字.

10.6 触发异常

到目前为止,我们所见到的异常都是由解释器引发的.由于执行期间的错误而引发.程序员在编写API 时也希望在遇到错误的输入时触发异常,为此,Python 提供了一种机制让程序员明确的触发异常:这就是raise 语句.

10.6.1 raise 语句

语法与惯用法

raise 语句对所支持是参数十分灵活,对应到语法上就是支持许多不同的格式.rasie 一般的用法是:

raise [SomeException [, args [, traceback]]]

第一个参数,SomeExcpetion,是触发异常的名字.如果有,它必须是一个字符串,类或实例(详见下文).如果有其他参数(arg 或traceback),就必须提供SomeExcpetion.Python 所有的标准异常见表10.2.

第二个符号为可选的args(比如参数,值),来传给异常.这可以是一个单独的对象也可以是一个对象的元组.当异常发生时,异常的参数总是作为一个元组传入.如果args 原本就是元组,那么就将其传给异常去处理;如果args 是一个单独的对象,就生成只有一个元素的元组(就是单元素元组).大多数情况下,单一的字符串用来指示错误的原因.如果传的是元组,通常的组成是一个错误字符串,一个错误编号,可能还有一个错误的地址,比如文件,等等.

最后一项参数,traceback,同样是可选的(实际上很少用它),如果有的话,则是当异常触发时新生成的一个用于异常-正常化(exception—normally)的追踪(traceback)对象.当你想重新引发异常时,第三个参数很有用(可以用来区分先前和当前的位置).如果没有这个参数,就填写None.

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

更多的特殊/少见的惯用法

当参数是一个实例的时候会发生什么呢? 该实例若是给定异常类的实例当然不会有问ti, 然而,

如果该实例并非这个异常类或其子类的实例时, 那么解释器将使用该实例的异常参数创建一个给定

异常类的新实例. 如果该实例是给定异常类子类的实例, 那么新实例将作为异常类的子类出现, 而

不是原来的给定异常类.

如果raise 语句的额外参数不是一个实例——作为替代,是一个单件(singleton)或元组—那么,

将用这些作为此异常类的初始化的参数列表.如果不存在第二个参数或是None,则参数列表为空.

如果SomeException 是一个实例,我们就无需对什么进行实例化了.这种情况下,不能有额外的参

数或只能是None.

异常的类型就是实例的类;也就是说,等价于触发此类异常,并用该实例为参数:比如 raise

instance.__class__,instance.

我们建议用异常类,不赞成用字符串异常.但如果用字符串作为SomeException,那么会触发一

个用字符串标识的异常,还有一个可选的参量(args)作参数.

最后,这种不含任何参数的raise 语句结构是在Python1.5 中新引进的,会引发当前代码块(code

block)最近触发的一个异常.如果之前没有异常触发,会因为没可以有重新触发的异常而生成一个

TypeError 异常.

由于raise 有许多不同格式有效语法(比如:SomeException 可以是类,实例或一个字符串),我们

提供表10.1 来阐明rasie 的不同用法.

表10.1 raise 语句的用法

rasie 语法 描述

raise exclass 触发一个异常,从exclass 生成一个实例(不含任何异常参数)

raise exclass() 同上,除了现在不是类;通过函数调用操作符(function calloperator:

"()")作用于类名生成一个新的exclass 实例,同样也没有异常参数

raise exclass, args 同上,但同时提供的异常参数args,可以是一个参数也可以元组

raise exclass(args) 同上

raise exclass,args, tb 同上,但提供一个追踪(traceback)对象tb 供使用

raise exclass,instance 通过实例触发异常(通常是exclass 的实例);如果实例是exclass

的子类实例,那么这个新异常的类型会是子类的类型(而不是

exclass);如果实例既不是exclass 的实例也不是exclass 子类的

实例,那么会复制此实例为异常参数去生成一个新的exclass 实例.

raise instance 通过实例触发异常: 异常类型是实例的类型; 等价于raise

instance.__class__, instance (同上).

raise string (过时的) 触发字符串异常

raise string, args 同上,但触发伴随着args

raise string, args, tb 同上,但提供了一个追踪(traceback)对象tb 供使用

raise (1.5 新增)重新触发前一个异常,如果之前没有异常,触发TypeError.

10.7 断言

断言是一句必须等价于布尔真的判定;此外,发生异常也意味着表达式为假.但在Python 中它们在运行时构建(与之相对的是编译期判别).断言可以简简单单的想象为raise-if 语句(更准确的说是raise-if-not 语句).测试一个表达式,如果返回值是假,触发异常.断言通过assert 语句实现.

10.7.1 断言语句

断言语句等价于这样的Python 表达式,如果断言成功不采取任何措施(类似语句),否则触发AssertionError(断言错误)的异常.assert 的语法如下:

assert expression[, arguments]

下面有一些演示assert 用法的语句:

assert 1 == 1
assert 2 + 2 == 2 * 2
assert len(['my list', 12]) < 10
assert range(3) == [0, 1, 2]

AssertionError 异常和其他的异常一样可以用try-except 语句块捕捉,但是如果没有捕捉,它将终止程序运行而且提供一个如下的traceback:

>>> assert 1 == 0
Traceback (innermost last): File "", line 1, in ?
AssertionError

如同先前章节我们研究的raise 语句,我们可以提供一个异常参数给我们的assert 命令:

>>> assert 1 == 0, 'One does not equal zero silly!' 
Traceback (innermost last):
File "", line 1, in ?
AssertionError: One does not equal zero silly!

下面是我们如何用try-except 语句捕获AssertionError 异常:

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!

想象一下断言语句在Python 中如何用函数实现.可以像下面这样:

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

此处的 if 语句检查 assert 的语法是否合适,也就是expr 必须是一个表达式.我们比较expr的类型和真正的表达式来确认.函数的第二部分对表达式求值然后根据结果选择性的引发异常.内建的变量__debug__在通常情况下为True,如果开启优化后为False(命令行选项-O)

10.8 标准异常

表10.2 列出了所有的Python 当前的标准异常集,所有的异常都是内建的. 所以它们在脚本启动前或在互交命令行提示符出现时已经是可用的了.

111129_yKjx_724288.png

111129_keYc_724288.png

所有的标准/内建异常都是从根异常派生的.目前,有3 个直接从BaseException 派生的异常子类:SystemExit,KeyboardInterrupt 和Exception.其他的所有的内建异常都是Exception 的子类.表10.2 中的每一层缩进都代表一次异常类的派生.

到了Python2.5,所有的异常的都是新风格(new-style)的类,并且最终都是BaseException 的子类.在这一版中,SystemExit 和KeyboardInterrupt 从Exception 的继承中移到BaseException 的继承下.这样可以允许如except Exception 的语句捕获所有非控制程序退出的异常.

10.9 *创建异常

你还是可以创建自己的异常.一种情况是你想在特定的标准异常和模块异常中添加额外的信息.我们将介绍两个例子,都与IOError 有关.

  • IOError 用于输入/输出的通用异常,可能在无效的文件访问或其他形式的通信中触发.假如我们想要更加明确的标明问ti的来源,比如:对于文件错误,我们希望有行为类似IOError 的一个FileError 异常

  • 另一个异常与套接字(socket) 网络编程有关.socket 模块生成的异常叫socket.error,不是内建的异常.它从通用Exception 类派生.然而socket.error 这个异常的宗旨和IOError 很类似,所以我们打算定义一个新的从IOError 派生的NetworkError 的异常,但是其包含了socket.error 提供的信息

我们现在给出一个叫做myexc.py 的模块和我们自定义的新异常FileError 与NetworkError.代码如10.2.

此模块定义了两个新的异常,FileError 和NetworkError, 也重新实现了一个诊断版的open()[myopen()]和socket.connect()[myconnect()].同时包含了一个测试函数[test()],当直接运行文件时执行.


行55-65

当网络连接失败时提供一个IOError 类型的异常.和一般的socket.error 不一样,我们还提供给程序员主机名和端口号.

我们试着去连接一个在远程主机上运行的程序,可能是某种服务. 因此我们需要知道主机名和服务器监听的端口.

当失败发生时,错误号和错误字符很有帮助,但是如果结合更精确的主机-端口会更有帮助,因为

这一对可能是由某个数据库或名称服务动态生成或重新获得.这些值由我们版本的connect()加入.

另一种情形是无法找到主机,socket.error 异常没有直接提供的错误号,我们为了遵循IOError 协议,

提供了一个错误号-错误字符串对,我们查找最接近的错误号.我们选用的ENXIO.

行67-73

类似同类myconnect(),myopen()也封装了已经存在的一些代码.这里,我们用的是open()函数.

我们仅仅捕捉IOError 异常.所有的其他都忽略并传给下一层(因为没有与他们相关的处理器).一旦

捕捉到IOError 我们引发我们自己的异常并通过fileArgs()返回值来定制参数.

行75-95

我们首先测试文件,这里使用testfile()函数.开始之前,我们需要新建一个测试文件,以便我们

可以手工的修改其权限来造成权限错误.这个tempfile 模块包含了创建临时文件文件名和临时文件

的代码.当前我们仅仅需要文件名,然后用myopen()函数来创建一个空的文件.注意,如果此次产生了

错误,我们不会捕获,我们的程序将致命的终止——测试程序当我们连文件都无法创建时不会继续.


我们的测试用了4 种不同的权限配置.零标示没有任何权限,0100 表示仅能执行,0400 表示只

读,0500 表示只可读或执行(0400+0100).在所有的情况下,我们试图用一种无效的方式打开文

件.os.chmod()被用来改变文件的权限(注意:这些权限有前导的零,表明他们是八进制[基数8]数)

如果发生错误,我们希望可以显示诊断的信息,类似Python 解释器捕获异常时所做的那样. 这就

是给出异常名和紧跟其后的异常的参数.__class__属性表示实例化该实例的类对象. 比在此显示完

整的类名(myexc.FileError)更好的做法是通过类对象的__name__属性来显示类名(FileError),这

也是异常未被捕获时你在解释器所见到的.随后是我们在封装函数中辛辛苦苦聚到一起的参数.

如果文件被打开成功,也就是权限由于某种原因被忽略.我们通过诊断信息指明并关闭文件.当

所有的测试都完成时,我们对文件开启所有的权限然后用os.unlink()移除(os.remove()等价于

os.unlink()).

行97-106

下一段代码(testnet())测试了我们的网络异常.套接字是一个用来与其他主机建立连接的通信

端点.我们创建一个套接字,然后用它连接一个没有接受我们连接的服务器的主机和一个不存在于我

们网络的主机.

行108-110

我们希望仅在直接调用我们脚本时执行test*()函数,此处的代码完成了该功能.大多数脚本用

同样的格式给出了这段文本.

在Unix 系的机器上运行这段脚本,我们得到了如下的输出:

$myexc.py

FileError: [Errno 13] 'r' Permission denied (perms: '---'):

'/usr/tmp/@18908.1'

FileError: [Errno 13] 'r' Permission denied (perms: '--x'):

'/usr/tmp/@18908.1'

FileError: [Errno 13] 'w' Permission denied (perms: 'r--'):

'/usr/tmp/@18908.1'

FileError: [Errno 13] 'w' Permission denied (perms: 'r-x'):

'/usr/tmp/@18908.1'

NetworkError: [Errno 146] Connection refused: 'deli:8080' NetworkError: [Errno 6] host

not found: 'www:8080'

在Win32 的机器上有些许的不同:

D:\python> python myexc.py

C:\WINDOWS\TEMP\~-195619-1 opened ok... perms ignored C:\WINDOWS\TEMP\~-195619-1

Edit By Vheavens

Edit By Vheavens

opened ok... perms ignored FileError: [Errno 13] 'w' Permission denied (perms: 'r-x'):

'C:\\WINDOWS\\TEMP\\~-195619-1'

FileError: [Errno 13] 'w' Permission denied (perms: 'r-x'):

'C:\\WINDOWS\\TEMP\\~-195619-1'

NetworkError: [Errno 10061] winsock error: 'deli:8080' NetworkError: [Errno 6] host

not found: 'www:8080'

你可以看到Windows 不支持文件的读权限,这就是前两次尝试文件打开成功的原因.在你的机器

和操作系统上的结果可能会大相径庭。

10.10 为什么用异常(现在)?

就现今应用来说, 普遍的是自洽的图形用户界面(GUIs)或是客户机/服务器体系, 例如Web.在应用层处理错误的能力近来变得更为重要, 用户已不再是应用程序的的唯一的直接运行者.随着互联网和网上电子商业应用越来越普及, web 服务器将成为应用软件的主要客户. 这意味着应用程序再也不能只是直接的失败或崩溃, 因为如果这样, 系统错误导致浏览器的错误, 这反过来又会让用户沮丧. 

运行环境必须足够强健,来处理应用级别的错误,并提供用户级别的错误信息.就服务器而言,这必须转化为一个"非错误" . 因为应用必须要成功的完成, 即使所做的不过是返回一个错误的信息, 向用户是提供一个有效的超文本标记语言(HTML)的网页指明错误.

如果你不清楚我在说什么, 那个一个简单的网页浏览器窗口,用大而黑的字体写到"内部服务器错误"是否更耳熟?用一个弹出式窗口宣告"文件中没有数据"的致命错误如何?作为一个用户, 这些词语对你有意义吗?没有, 当然没有(除非你是一个互联网软件工程师), 至于对普通用户来说,这些是无休止的混乱和挫折感的来源. 这些错误导致在执行的程序时的失败. 应用不论是返回无效的超文本传输协议( http)数据还是致命地终止, 都会导致Web 服务器举手投降, 说: "我放弃" !

这种类型的执行错误不应该被允许, 无论情况如何. 随着系统变得更加复杂, 又牵涉到更多的新手用户, 要采取额外的措施, 确保用户平滑的学到应用经验. 即使面对一个错误, 应用应该成功的中止, 不至于灾难性的影响其执行环境. Python 异常处理促使成熟和正确的编程.

10.11 到底为什么要异常?

如果上文的动机不够充分, 试想Python 编程没有程序级的异常处理. 第一件事需要担心的是客户端程序员在自己的代码中遗忘控制. 举例来说, 如果你创造了一个交互的应用程序分配并使用了大量的资源, 如果一个用户击中Ctrl+C 或其他键盘中断, 应用程序将不会有机会执行清理工作, 可能导致数据丢失或数据损坏. 此外, 也没有机制来给出可选的行为, 诸如提示用户, 以确认他们真的是想退出或是他们意外的按下了Ctrl 键.

另一个缺点就是函数必须重写来为错误的情形返回一个"特殊"的值, 如:None. 程序员要负责检查每一个函数调用的返回值. 这可能是个麻烦, 因为你可能不得不检查返回值, 这和没有发生错误时你期待结果也许不是同一类型的对象. 什么,你的函数要把None 作为一个有效的数值返回?那么, 你将不得不拿出另一个返回值, 也许是负数.我们也许并不需要提醒你, 在Python 的环境下负数下可能是有效的, 比如作为一个序列的索引. 作为一个写应用程序接口( API )的程序员, 你不得不为每个一个用户输入可能遇到的返回错误写文档. 同时, 我们难以(而且乏味)在多层次的代码中以传播错误(和原因).没有一个简单的传播方法像异常一样做到这一点. 因为错误的数据需要在调用层次中向上转发,但在前进的道路上可能被曲解. 一个不相干的错误可能会被宣布为起因,而实际上它与原始问ti完全无关.在一层一层的传递中,我们失去了对原始错误封装和保管的能力,更不用说完全地失去我们原本关心的数据的踪影!异常不仅简化代码, 而且简化整个错误管理体系 --- 它不该在应用开发中此重要角色;而有了Python 的异常处理能力, 也的确没有必要了.

10.12 异常和sys 模块

另一种获取异常信息的途径是通过sys 模块中exc_info()函数. 此功能提供了一个3 元组(3-tuple)的信息, 多于我们单纯用异常参数所能获得. 让我们看看如何用sys.exc_info() :

>>> try:
...     float('abc123')
... except:
...     import sys
...     exc_tuple = sys.exc_info()
...
>>> print exc_tuple
()
>>> for eachItem in exc_tuple:
... print eachItem
... exceptions.ValueError
invalid literal for float(): abc123

我们从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()来代替. 在未来版本Python 中,所有这三个变量都将被逐步停用,并最终移除.

10.13 相关模块

表10.3 异常相关的标准库

144052_yTXY_724288.png


转载于:https://my.oschina.net/cqlcql/blog/657475

本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 162202241@qq.com 举报,一经查实,本站将立刻删除。

最新评论

欢迎您发表评论:

请登录之后再进行评论

登录
相关推荐