with语句在我们的日常Python代码编写中时常会用到,我们通常知道可以用with语句来代替try…except…finally这样的写法,但是为什么它能够替代,如果在with中发生了异常怎么处理,这背后的原理却是并不是很明了。
最权威的说法肯定是来自官方文档的说法。
先放出自己的小总结,然后翻译一下官方文档的with语句章节和with语句的上下文管理器章节。
地址在此
with是在2.5版本中引入的,with用于包装一个方法由上下文管理器(context manager)定义的代码块。with允许通常的 try…except…finally的使用模式被封装来方便使用。
with_stmt ::= "with" with_item ("," with_item)* ":" suite
with_item ::= expression ["as" target]
具有一个“item”的with语句的运行如下:
对于超过一个项这样的情况,上下文管理器被处理得就像多个with语句被嵌套一样:
with A() as a, B() as b:
suite
和如下等价
with A() as a:
with B() as b:
suite
地址在此
一个上下文管理器(context manager)是一个对象,其定义了运行一个with语句时候要建立的运行时上下文(runtime context)。上下文管理器掌控了何处进入,何处退出以及一个代码块运行所需的运行时上下文。上下文管理器通常在使用with语句的时候调用,但是也可以通过直接调用它们的方法来使用。
上下文管理器的典型使用包括存储和恢复各种全局状态,锁和解锁资源,关闭打开的文件等。
要获得更多上下文管理器相关信息,参考上下文管理器类型。
object.__enter__(self)
进入和这个对象相关的运行时上下文,with语句会将这个方法的返回值绑定到用as语句指定的特定目标(如果有的话)。
object.__exit__(self, exc_type, exc_value, traceback)
离开和这个对象相关的运行时上下文,参数描述了导致离开上下文的异常。如果不需要异常离开上下文,所有的参数将会是None。
如果引入了异常,并且该方法希望抑制异常(例如,阻止异常的传播),则它应该返回true。否则在退出这个方法的时候,异常将会被正常处理。
注意,__exit__()方法不应该重新抛出传入的异常,这是调用者的职责。
我们最常用的with语句莫过于
with open(file) as f
了吧,那么这个背后的原理是什么呢?
先看看__builtin__.py中的open
def open(name, mode=None, buffering=None): # real signature unknown; restored from __doc__
"""
open(name[, mode[, buffering]]) -> file object
Open a file using the file() type, returns a file object. This is the
preferred way to open a file. See file.__doc__ for further information.
"""
return file('/dev/null')
本质上就是返回一个file对象,再看看file对象(Python源代码中的Objects/fileobject.c),
{"__enter__", (PyCFunction)file_self, METH_NOARGS, enter_doc},
__enter__()指向的是file_self。
static PyObject *
file_self(PyFileObject *f)
{
if (f->f_fp == NULL)
return err_closed();
Py_INCREF(f);
return (PyObject *)f;
}
所以__enter__()本质上就是返回一个file的对象。再看看__exit__()
{"__exit__", (PyCFunction)file_exit, METH_VARARGS, exit_doc}
然后其指向的方法
static PyObject *
file_exit(PyObject *f, PyObject *args)
{
PyObject *ret = PyObject_CallMethod(f, "close", NULL);
if (!ret)
/* If error occurred, pass through */
return NULL;
Py_DECREF(ret);
/* We cannot return the result of close since a true
* value will be interpreted as "yes, swallow the
* exception if one was raised inside the with block". */
Py_RETURN_NONE;
}
可以看到,在__exit__()中间执行了f.close(),所以就不用我们自己再去手动执行了。同时返回值并不为true,所以任何的错误都会抛出。
是时候自己玩一下了
# 玩with语句
class TestWith(object):
def __init__(self):
print "this is init of TestWith"
def if_exist(self):
print "I'm here"
def __enter__(self):
print "This is TestWith __enter__"
return self
def __exit__(self, *args):
for i in args:
print i
print "Now __exit__"
with TestWith() as t:
t.if_exist()
按照设想,首先得到TestWith的对象,然后t会得到__enter__()的结果,就是这个对象自身,然后调用一下if_exist函数,最后退出。运行的结果为
this is init of TestWith
This is TestWith __enter__
I'm here
None
None
None
Now __exit__
可见符合我们预期,并且在没有异常的时候传入__exit__()的为三个None。如果抛出了异常呢?
with TestWith() as t:
1/0
t.if_exist()
结果为
this is init of TestWith
This is TestWith __enter__
<type 'exceptions.ZeroDivisionError'>
integer division or modulo by zero
<traceback object at 0x1124558c0>
Now __exit__
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
<ipython-input-9-53bd54b7b92a> in <module>()
18
19 with TestWith() as t:
---> 20 1/0
21 t.if_exist()
ZeroDivisionError: integer division or modulo by zero
由于1/0抛出异常,所以在这里退出,然后将异常传入到__exit__()中,由于__exit__()没有返回true,所以会重新抛出异常。
那如果我们改一下__exit__()函数,让它返回True呢?
class TestWith(object):
def __init__(self):
print "this is init of TestWith"
def if_exist(self):
print "I'm here"
def __enter__(self):
print "This is TestWith __enter__"
return self
def __exit__(self, *args):
for i in args:
print i
print "Now __exit__"
return True
with TestWith() as t:
1/0
t.if_exist()
我们得到输出如下:
this is init of TestWith
This is TestWith __enter__
<type 'exceptions.ZeroDivisionError'>
integer division or modulo by zero
<traceback object at 0x1124553b0>
Now __exit__
可见,确实是不会抛出异常。