上一篇(Python 异常处理 (二))中,我们了解了如何使用traceback模块和logging模块获取异常信息。这一篇,我们将讲述有关于with,assert,raise的相关知识。
5.with
with 语句是从 Python 2.5 开始引入的一种与异常处理相关的功能(2.5 版本中要通过 from __future__ import with_statement
导入后才可以使用),从 2.6 版本开始缺省可用。with 语句作为 try/finally 编码范式的一种替代,适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的“清理”操作,释放资源,比如文件使用后自动关闭、线程中锁的自动获取和释放等。
上面的这段解释略显生硬,不如我们先来对比着看两个例子,体会一下with的简单用法,然后再来进行深入讲解:
>>> try:
... fout = open("test.txt", "w")
... fout.write("this is a test")
... finally:
... fout.close()
>>> with open("test.txt", "w") as fout:
... fout.write("this is a test")
以上两段代码要做的事情是完全一致的。在第一个例子中,我们打开了一个文件,为了确保在操作文件的过程中无论是否发生异常,文件都能被关闭,我们采用了try/finally句式;而在第二个例子中,我们用with/as替代了try/finally,这种句式同样能够做到对文件的及时关闭。比较起来,使用 with 语句可以减少编码量,显得更加优雅,所以我们建议使用后者。
那么with到底是如何工作的呢?下面我们来作一个详细的解释。
5.1.with如何工作
with语句操作的对象只能是上下文管理器,要使用 with 语句,首先要明白上下文管理器这一概念。故而,我们来解释几个术语:
- 上下文管理协议(Context Management Protocol):包含方法
__enter__()
和__exit__()
。 - 上下文管理器(Context Manager):支持上下文管理协议的对象,这种对象实现了
__enter__()
和__exit__()
方法。上下文管理器定义执行 with 语句时要建立的运行时上下文,负责执行 with 语句块上下文中的进入与退出操作。通常使用 with 语句调用上下文管理器,也可以通过直接调用其方法来使用。 - 运行时上下文(runtime context):由上下文管理器创建,通过上下文管理器的
__enter__()
和__exit__()
方法实现,__enter__()
方法在语句体执行之前进入运行时上下文,__exit__()
在语句体执行完后从运行时上下文退出。with 语句支持运行时上下文这一概念。 - 上下文表达式(context expression):with 语句中跟在关键字 with 之后的表达式,该表达式要返回一个上下文管理器对象。
- 语句体(with-body):with 语句包裹起来的代码块,在执行语句体之前会调用上下文管理器的
__enter__()
方法,执行完语句体之后会执行__exit__()
方法。
with语句的语法格式如下:
with context_expression [as target(s)]:
with-body
这里 context_expression 要返回一个上下文管理器对象,该对象并不赋值给 as 子句中的 target(s) ,如果指定了 as 子句的话,会将上下文管理器的__enter__()
方法的返回值赋值给 target(s)。target(s) 可以是单个变量,或者由“()”括起来的元组(不能是仅仅由“,”分隔的变量列表,必须加“()”)。
with语句的执行过程如下:
context_manager = context_expression
exit = type(context_manager).__exit__
value = type(context_manager).__enter__(context_manager)
exc = True # True 表示正常执行,即便有异常也忽略;False 表示重新抛出异常,需要对异常进行处理
try:
try:
target = value # 如果使用了 as 子句
with-body # 执行 with-body
except:
# 执行过程中有异常发生
exc = False
# 如果 __exit__ 返回 True,则异常被忽略;如果返回 False,则重新抛出异常
# 由外层代码对异常进行处理
if not exit(context_manager, *sys.exc_info()):
raise
finally:
# 正常退出,或者通过 statement-body 中的 break/continue/return 语句退出
# 或者忽略异常退出
if exc:
exit(context_manager, None, None, None)
# 缺省返回 None,None 在布尔上下文中看做是 False
1.执行 context_expression,生成上下文管理器 context_manager
2.调用上下文管理器的__enter__()
方法;如果使用了 as 子句,则将__enter__()
方法的返回值赋值给 as 子句中的 target(s)
3.执行语句体 with-body
4.不管是否执行过程中是否发生了异常,执行上下文管理器的__exit__()
方法,__exit__()
方法负责执行“清理”工作,如释放资源等。如果执行过程中没有出现异常,或者语句体中执行了语句 break/continue/return,则以 None 作为参数调用 __exit__(None, None, None)
;如果执行过程中出现异常,则使用 sys.exc_info 得到的异常信息为参数调用 __exit__(exc_type, exc_value, exc_traceback)
5.出现异常时,如果__exit__(type, value, traceback)
返回 False,则会重新抛出异常,让with 之外的语句逻辑来处理异常,这也是通用做法;如果返回 True,则忽略异常,不再对异常进行处理
上面对于with执行过程的阐述可能略显生硬,建议读者参考:。其中有一个非常详细的例子,结合例子来看这里的解释,会理解的更加透彻。
5.2.自定义上下文管理器
开发人员可以自定义支持上下文管理协议的类,来对软件系统中的资源进行管理,比如数据库连接、共享资源的访问控制等。自定义的上下文管理器要实现上下文管理协议所需要的__enter__()
和__exit__()
两个方法:
- context_manager.__enter__():进入上下文管理器的运行时上下文,在语句体执行前调用。with 语句将该方法的返回值赋值给 as 子句中的 target,如果指定了 as 子句的话(否则返回值会被扔掉);
- context_manager.__exit__(exc_type, exc_value, exc_traceback):退出与上下文管理器相关的运行时上下文,返回一个布尔值表示是否对发生的异常进行处理。参数表示引起退出操作的异常,如果退出时没有发生异常,则3个参数都为None。如果发生异常,返回True 表示不处理异常,否则会在退出该方法后重新抛出异常以由 with 语句之外的代码逻辑进行处理。
下面通过一个简单的示例来演示如何构建自定义的上下文管理器。注意,上下文管理器必须同时提供 __enter__()
和__exit__()
方法的定义,缺少任何一个都会导致 AttributeError;with 语句会先检查是否提供了__exit__()
方法,然后检查是否定义了__enter__()
方法。
#!/usr/bin/python
#coding:utf8
import sys
reload(sys)
sys.setdefaultencoding("utf8")
class DummyResource:
def __init__(self, tag):
self.tag = tag
print 'Resource [%s]' % tag
def __enter__(self):
print '[Enter %s]: Allocate resource.' % self.tag
return self # 可以返回不同的对象
def __exit__(self, exc_type, exc_value, exc_tb):
print '[Exit %s]: Free resource.' % self.tag
if exc_tb is None:
print '[Exit %s]: Exited without exception.' % self.tag
else:
print '[Exit %s]: Exited with exception raised.' % self.tag
return False # 可以省略,缺省的None也是被看做是False
#with-ex.1
with DummyResource('Normal'):
print '[with-body] Run without exceptions.'
print "----------**----------"
#with-ex.2
with DummyResource('With-Exception'):
print '[with-body] Run with exception.'
raise Exception
print '[with-body] Run with exception. Failed to finish statement-body!'
DummyResource中的__enter__()
返回的是自身的引用,这个引用可以赋值给as子句中的 target 变量,这样target本身就是一个上下文管理器对象;返回值的类型可以根据实际需要设置为不同的类型,不必是上下文管理器对象本身。执行结果如下:
Resource [Normal]
[Enter Normal]: Allocate resource.
[with-body] Run without exceptions.
[Exit Normal]: Free resource.
[Exit Normal]: Exited without exception.
----------**----------
Resource [With-Exception]
[Enter With-Exception]: Allocate resource.
[with-body] Run with exception.
[Exit With-Exception]: Free resource.
[Exit With-Exception]: Exited with exception raised.
Traceback (most recent call last):
File "test_with.py", line 32, in <module>
raise Exception
Exception
第一个with中,正常执行时会先执行完语句体with-body,然后执行 __exit__()
方法释放资源;第二个with中,with-body中发生异常时with-body并没有执行完,但资源会保证被释放掉,同时产生的异常由with语句之外的代码逻辑来捕获处理。
6.assert
assert,即断言,直接看语法:
assert expression [,arguments]
其中assert是断言的关键字。执行该语句的时候,先判断表达式expression,如果表达式为真,则什么都不做;如果表达式不为真,则抛出AssertionError异常,传进去的字符串arguments则会作为异常类的实例的具体信息存在。来看一个例子:
>>> try:
... assert 1==2,"1 is not equal 2!"
... except AssertionError as e:
... print e
...
1 is not equal 2!
>>> type(e)
<type 'exceptions.AssertionError'>
7.raise
如果我们想要在自己编写的程序中主动抛出异常,该怎么办呢?raise语句可以帮助我们达到目的。其基本语法如下:
raise [SomeException [, args [,traceback]]
第一个参数,SomeException必须是一个异常类,或异常类的实例;第二个参数是传递给SomeException的参数,必须是一个元组,这个参数用来传递关于这个异常的有用信息;第三个参数traceback很少用,是一个traceback对象。
来看两个例子:
>>> try:
... raise NameError("this is a test")
... except NameError as e:
... print e
...
this is a test
>>> type(e)
<type 'exceptions.NameError'>
>>> try:
... raise NameError,"this is a test"
... except NameError as e:
... print e
...
this is a test
>>> type(e)
<type 'exceptions.NameError'>
以上两个例子的结果是一致的,但是传入的参数不同:前者传入的是一个异常类的对象,后者传入的是异常类+参数args。
还有一种情况,叫做传递异常(re-raise Exception),即捕捉到了异常,但是又想重新引发它,我们先来看个例子:
>>> try:
... try:
... raise IOError
... except IOError:
... print "inner exception"
... raise
... except IOError:
... print "outter exception"
...
inner exception
outter exception
这个是将拦截到的异常错误原样抛出扔给上层处理,处理过程是:首先被内层IOError异常捕获,打印“inner exception”, 然后把相同的异常再抛出,被外层的except捕获,打印”outter exception”。在Python2中,为了保持异常的完整信息,那么你捕获后再次抛出时千万不能在raise后面加上异常对象,否则你的trace信息就会从此处截断。以上是最简单的重新抛出异常的做法。
还有一些技巧可以考虑,比如抛出异常前对异常的信息进行更新:
>>> try:
... try:
... a=1/0
... except ZeroDivisionError as e:
... print e
... e.args += ('more info',)
... print e
... raise
... except ZeroDivisionError as e:
... print e
...
integer division or modulo by zero
('integer division or modulo by zero', 'more info')
('integer division or modulo by zero', 'more info')
本文中我们讲了关于with,assert,raise的知识,下一篇(Python 异常处理 (四))我们将介绍关于用户自定义异常的相关内容。
参考文献
[1] http://www.runoob.com/python/python-exceptions.html
[4] http://www.tuicool.com/articles/f2uumm
[5] https://docs.python.org/2.7/library/sys.html
[6] http://www.2cto.com/kf/201303/194676.html
[7] http://python.usyiyi.cn/python_278/library/sys.html
[8] https://docs.python.org/2.7/library/traceback.html
[9] https://docs.python.org/2.7/library/exceptions.html
[11] https://www.ibm.com/developerworks/cn/opensource/os-cn-pythonwith/
[13] https://docs.python.org/release/2.6/whatsnew/2.6.html
[14] http://python3-cookbook.readthedocs.io/zh_CN/latest/c14/p08_creating_custom_exceptions.html
[15] https://docs.python.org/2.7/tutorial/errors.html
以上是本系列的全部参考文献,对原作者表示感谢。