上下文管理器其实是with语句,这是为了简化try/finally模式,这可以保证一段代码在运行完之后,即使出现错误也能正确的运行。finally的语句用于释放重要资源,比如数据库和文件
的句柄,或者还原临时变更的对象,例如锁。
就跟前面的系列文章所述,上下文管理器也是一种协议,包含__enter__和__exit__方法。在with语句开始运行是会调用__enter__方法,结束后会调用__exit__方法。最常见的例子就是打开文件。
打开所在项目的文件
with open('List.py',encoding = 'utf8') as f:
src = f.read()
f
Out[4]: <_io.TextIOWrapper name='List.py' mode='r' encoding='utf8'>
f.closed, f.encoding
Out[5]: (True, 'utf8')
我们可以看出f这个变量依然可以用,但是文件句柄已经关闭了。as语句只不过是把值绑定到了目标变量,as语句是可选的,但是如果是打开文件或者是连接数据库,则必须绑定获得句柄。
说了这么多,这意味着我们也可以制造一个上下文管理器,只要实现了__enter__和__exit__方法。
class test():
def __enter__(self):
print('enter')
return 'enter'
def __exit__(*args):
print(args)
return 'exit'
with test() as f:
print('sdad')
raise Zero("dsad")
显示出来的结果如下:
enter
sdad
(<__main__.test object at 0x0000015C1CE351D0>, <class 'NameError'>, NameError("name 'Zero' is not defined",), <traceback object at 0x0000015C1CE27C08>)
再看看f变量:
f
Out[3]: 'enter'
我们自己定义了一个类,里面包含了__enter__和__exit__方法,在with语句开始时,你会发现显示屏上出现了'enter',这印证了之前的话,__enter__会在with语句刚开始时执行,并且
把return的结果反馈回f变量。进入到语句块之后,会显示出'sdad',知道报错,然后你会发现__exit__里面接受了四个变量,分别是:
--self:本身的实例
--exc_type:异常类(比如NameError)
--exc_value:异常的实例,可以通过exc_value.args获取参数
--traceback:traceback对象,Extract, format and print information about Python stack traces.也就是异常栈
更详细的信息在:https://docs.python.org/3.5/library/stdtypes.html#typecontextmanager
其实每次这样定义会很累,官方提供了contextlib模块来简化开发。
里面一共包含了:
__all__ = ["contextmanager", "closing", "ContextDecorator", "ExitStack",
"redirect_stdout", "redirect_stderr", "suppress"]
其中,contextmanager可以把简单的生成器函数变成上下文管理器。这个也是用了yield语句,但是这个却与迭代无关。
官网给出的例子如下:
from contextlib import contextmanager
@contextmanager
def tag(name):
print("<%s>" % name)
yield
print("</%s>" % name)
with tag('woshi'):
print('love')
<woshi>
love
</woshi>
结果如上通过yield语句将函数的定义体分成了两部分,yield前面的语句执行__enter__,后面的语句执行__exit__,我们可以改变test类
@contextmanager
def text():
print('enter')
yield 'enter'
print('exit')
with text() as f:
print('sd')
enter
sd
exit
class _GeneratorContextManager(ContextDecorator):
"""Helper for @contextmanager decorator."""
def __init__(self, func, args, kwds):
self.gen = func(*args, **kwds)
self.func, self.args, self.kwds = func, args, kwds
# Issue 19330: ensure context manager instances have good docstrings
doc = getattr(func, "__doc__", None)
if doc is None:
doc = type(self).__doc__
self.__doc__ = doc
# Unfortunately, this still doesn't provide good help output when
# inspecting the created context manager instances, since pydoc
# currently bypasses the instance docstring and shows the docstring
# for the class instead.
# See http://bugs.python.org/issue19404 for more details.
def _recreate_cm(self):
# _GCM instances are one-shot context managers, so the
# CM must be recreated each time a decorated function is
# called
return self.__class__(self.func, self.args, self.kwds)
def __enter__(self):
try:
return next(self.gen)
except StopIteration:
raise RuntimeError("generator didn't yield") from None
def __exit__(self, type, value, traceback):
if type is None:
try:
next(self.gen)
except StopIteration:
return
else:
raise RuntimeError("generator didn't stop")
else:
if value is None:
# Need to force instantiation so we can reliably
# tell if we get the same exception back
value = type()
try:
self.gen.throw(type, value, traceback)
raise RuntimeError("generator didn't stop after throw()")
except StopIteration as exc:
# Suppress StopIteration *unless* it's the same exception that
# was passed to throw(). This prevents a StopIteration
# raised inside the "with" statement from being suppressed.
return exc is not value
except RuntimeError as exc:
# Likewise, avoid suppressing if a StopIteration exception
# was passed to throw() and later wrapped into a RuntimeError
# (see PEP 479).
if exc.__cause__ is value:
return False
raise
except:
# only re-raise if it's *not* the exception that was
# passed to throw(), because __exit__() must not raise
# an exception unless __exit__() itself failed. But throw()
# has to raise the exception to signal propagation, so this
# fixes the impedance mismatch between the throw() protocol
# and the __exit__() protocol.
#
if sys.exc_info()[1] is not value:
raise
从源码可以看出:
@contextmanager 实际上是把函数包装成了一个类,
__enter__:调用生成器函数,保存生成器对象,如上所述是,next(self.gen),调用到yield关键字所在的位置,返回这个值绑定到as后面的变量。
__exit__:检查有没有异常传给exc_type,没有的话继续调用之后的代码,否则调用self.gen.throw(type, value, traceback)抛出异常。