介绍
contextlib模块包含的工具可以用于处理上下文管理器和with语句
上下文管理器API
'''
上下文管理器(context manager)负责管理一个代码块中的资源,会在进入代码块时创建资源,然后再退出代码后清理这个资源。
比如:文件就支持上下文管理器API,可以确保文件读写后关闭文件。
with open("xxx") as f:
f.read()
'''
# 那么这是如何实现的呢?我们可以手动模拟一下
class Open:
def __init__(self, filename, mode='r', encoding=None):
self.filename = filename
self.mode = mode
self.encoding = encoding
def __enter__(self):
print("__enter__,有了这个就可以使用with Open() as xx语句,这里的xx就是我return的内容")
return self
def read(self):
print(f"文件进行读操作,读取文件:{self.filename}, 模式:{self.mode}, 编码:{self.encoding}")
def __exit__(self, exc_type, exc_val, exc_tb):
print("__exit__,我是用来清理资源的,当操作执行完毕之后就会执行我,比如:关闭文件")
# 首先执行Open("1.xxx"),实例化一个对象,然后调用__enter__
# __enter__返回的内容会叫给f,然后执行with语句块里面的代码
# with语句块代码执行完毕之后,再执行__exit__
with Open("1.xxx") as f:
f.read()
'''
__enter__,有了这个就可以使用with Open() as xx语句,这里的xx就是我return的内容
文件进行读操作,读取文件:1.xxx, 模式:r, 编码:None
__exit__,我是用来清理资源的,当操作执行完毕之后就会执行我,比如:关闭文件
'''
因此需要注意的是,里面的f只是__enter__
返回的值,并不是真正意义上的self,怎么理解呢?我们来看一个例子
class Open:
def __init__(self, filename, mode='r', encoding=None):
self.filename = filename
self.mode = mode
self.encoding = encoding
def __enter__(self):
print("__enter__,有了这个就可以使用with Open() as xx语句,这里的xx就是我return的内容")
return None
def __exit__(self, exc_type, exc_val, exc_tb):
print("__exit__,我是用来清理资源的,当操作执行完毕之后就会执行我,比如:关闭文件")
with Open("1.xxx") as f:
print(f)
"""
__enter__,有了这个就可以使用with Open() as xx语句,这里的xx就是我return的内容
None
__exit__,我是用来清理资源的,当操作执行完毕之后就会执行我,比如:关闭文件
"""
# 我们看到此时__enter__返回的是None,那么对应的f也是None
# 当with语句执行完毕之后,并不是调用f.__exit__或者说Open.__exit__(f, ...)
# 而是说当执行with Open("1.xxx")的时候,已经创建了一个实例对象,只不过这个实例对象是什么我们不知道
# 但是as f,只是调用了这个实例对象的__enter__,然后将返回值赋值给了f
# 然后with语句结束,也是通过这个实例对象来调用__exit__,而不是f,这一点需要记清楚
# 所以要记住f是由__enter__的返回值决定的,只不过大多数情况下,__enter__里面返回的都是self本身,所以相应的f指向的也是该类的实例对象
# 因此这个例子我们也可以改写一下
class Girl:
def __init__(self, name, age):
self.name = name
self.age = age
def __enter__(self):
return "返回点什么吧"
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"我会被调用吗")
with Girl("satori", 16) as f:
print("f", f)
"""
f 返回点什么吧
我会被调用吗
"""
# 显然此时f只是一个字符串,跟Girl的实例对象没有任何关系
# 再或者我们先把这个实例对象创建出来
g = Girl("hanser", 27)
# 此时__enter__、__exit__都是由g去调用,跟f没有关系
with g as f:
print("f", f)
"""
f 返回点什么吧
我会被调用吗
"""
因此with语句的流程我们已经清晰了,就是三步
创建实例对象,执行__enter__,然后将其返回值交给as xx中的xx
执行with语句的代码
最后执行__exit__,显然__exit__是进行收尾工作的。
但是我们发现__exit__
里面除了self之外,还有三个参数exc_type, exc_val, exc_tb,显然这三个参数分别是异常类型、异常值、异常的堆栈
class Open:
def __init__(self, filename, mode='r', encoding=None):
self.filename = filename
self.mode = mode
self.encoding = encoding
def __enter__(self):
return 123
def __exit__(self, exc_type, exc_val, exc_tb):
# 注意到这里有三个参数,使用pycharm的时候,会很智能地自动帮我们加上去
print(exc_type)
print(exc_val)
print(exc_tb)
return True
# 由于没有任何异常,所以exc_type, exc_val, exc_tb均为None
with Open("1.xx") as f:
print(f)
'''
123
None
None
None
'''
# with语句当中出现了异常
with Open("1.xx") as f:
print(f)
1 / 0
print(123)
print(456)
print(789)
print("你猜我会被执行吗?")
'''
123
<class 'ZeroDivisionError'>
division by zero
<traceback object at 0x0000000009EDD848>
你猜我会被执行吗?
'''
# 解释说明
'''
可以看到当我们程序没有出错的时候,打印的值全为None。一旦with语句里面出现了异常,那么会立即执行__exit__函数。
里面的参数就是:异常的类型,异常的值,异常的信息栈。
因此:当with语句结束之后会调用__exit__函数,如果with语句里面出现了错误则会立即调用__exit__函数。
但是__exit__函数返回了个True是什么意思呢?
当with语句里面出现了异常,理论上是会报错的,但是由于要执行__exit__函数,所以相当于暂时把异常塞进了嘴里。
如果__exit__函数最后返回了一个布尔类型为True的值,那么会把塞进嘴里的异常吞下去,程序不报错正常执行。如果返回布尔类型为False的值,会在执行完__exit__函数之后再把异常吐出来,引发程序崩溃。
这里我们返回了True,因此程序正常执行,最后一句话被打印了出来。
但是1/0这句代码后面的几个print却没有打印,为什么呢?
因为上下文管理执行是有顺序的,
with Open("1.xxx") as f:
code1
code2
先执行Open函数的__init__函数,再执行__enter__函数,把其返回值给交给f,然后执行with语句里面的代码,最后执行__exit__函数。
只要__exit__函数执行结束,那么这个with语句就算结束了。
而with语句里面如果有异常会立即进入__exit__函数,因此异常语句后面的代码是无论如何都不会被执行的。
'''
上下文管理器作为函数修饰符
类ContextDecorator增加了对常规上下文管理器类的支持,因此不仅可以作为上下文管理器,也可以作为函数修饰符
import contextlib
class Context(contextlib.ContextDecorator):
def __init__(self, how_used):
self.how_used = how_used
print(f"__init__({self.how_used})")
def __enter__(self):
print(f"__enter__({self.how_used})")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"__exit__({self.how_used})")
# 此时我们定义一个函数就可以用Context这个类的实例对象去装饰
@Context("我要作为装饰器去装饰") # __init__(我要作为装饰器去装饰)
def foo(name):
print(name)
return f"我是汽车老司机,不不不,我是小司机"
# 现在的foo已经不再是原来的foo了,至于它现在到底是什么,我们后面说
# 然后此时如果再调用foo,那么会先执行Context的__enter__方法,然后执行原来的foo函数的逻辑,最后调用Context的__exit__方法
print(foo("hanser"))
"""
__enter__(我要作为装饰器去装饰)
hanser
__exit__(我要作为装饰器去装饰)
我是汽车老司机,不不不,我是小司机
"""
# 可能有人好奇,为什么返回值打印是在最后,因为这是print啊
# foo("hanser")虽然已经执行完毕了,但是外面的print肯定要等__exit__结束之后才行
但是这是如何实现的呢?首先我们装饰foo的时候,显然是使用Context的实例对象去装饰的,相当于给这个实例对象加上了括号,并且把foo这个函数作为参数传进去了。既然实例对象加上了括号(调用)
,这就意味着该实例对象对应的类一定有__call__
方法,但是我们定义的没有,那么继承的父类肯定有。我们来看一下源码
class ContextDecorator(object):
def _recreate_cm(self):
return self
def __call__(self, func):
@wraps(func)
def inner(*args, **kwds):
with self._recreate_cm():
return func(*args, **kwds)
return inner
# 类的源码很少,当我们使用实例对象去装饰foo函数的时候,就相当于给实例对象加上了括号,那么肯定要走这里的__call__方法
# 但是这个self可不是ContextDecorator的self,而是我们之前定义的Context类里面的self,也就是Context的实例对象
# 如果面向对象基础不好的话,建议去复习一下,这里简单说一下。
# 调用Context类实例对象的时候,肯定走Context里面的__call__方法,但是没有,那么会调用父类的,但是调用时对应的self还是Context的self
# 因此之前的@Context("我要作为装饰器去装饰"),就等价于
"""
context = Context("我要作为装饰器去装饰")
@context
"""
# 然后再装饰foo的时候,相当于foo = context(foo),那么会调用这里的__call__方法,然后foo会被传递给这里的func,这里返回inner
# 所以foo在被装饰完之后,就相当于这里的inner,只不过有@wraps(func)这个装饰器在,所以装饰之后的函数名、__doc__等元信息没有改变
# 那么当我再调用foo("hanser")的时候,就等价于调用这里的inner("hanser")
# 而self._recreate_cm()返回的就是self,这个self就是我们的context,或者Context的实例对象
# 所以with self._recreate_cm():就是with self:
# 现在就很清晰了,所以要先走Context里面的__enter__,然后return func(*args, **kwds),这里的func显然就是原来真正的foo
# 执行完毕之后,拿到返回值,然后执行__exit__,最后最外层的print再将拿到的返回值打印
希望能仔细理清一遍这里的流程
从生成器到上下文管理器
采用传统方式创建上下文管理器并不难,只需要包含一个__enter__
方法和一个__exit__
方法的类即可。 不过某些时候,如果只有很少的上下文需要管理,那么完整地写出所以代码便会成为额外的负担。 在这些情况下,可以使用contextmanager修饰符将一个生成器函数转换为上下文管理器。
import contextlib
"""
代码结果
@contextlib.contextmanager
def foo():
print(123)
yield 456
print(789)
with foo() as f:
print(f)
123
456
789
只要给函数加上这个装饰器,那么便可以使用with as 语句。
当中的yield相当于将代码块分隔为两个战场:
yield上面的代码相当于__enter__会先执行,然后将yield的值交给f,然后执行with语句,最后执行yield下面的代码块,相当于__exit__
"""
@contextlib.contextmanager
def bar(name, age):
print(f"name is {name}, age is {age}")
yield list
print("我是一匹狼,却变成了狗")
with bar("mashiro", 16) as b:
print(b("abcde"))
'''
name is mashiro, age is 16
['a', 'b', 'c', 'd', 'e']
我是一匹狼,却变成了狗
'''
# 先执行yield上面的内容,然后yield list,那么b = list,最后执行yield下面的内容
contextmanager返回的上下文管理器派生自ContextDecorator,所以也可以被用作函数修饰符
import contextlib
@contextlib.contextmanager
def bar(name, age):
print(f"name is {name}, age is {age}")
yield
print("我是一匹狼,却变成了狗")
@bar("satori", 16)
def foo():
print("猜猜我会在什么地方输出")
foo()
'''
name is satori, age is 16
猜猜我会在什么地方输出
我是一匹狼,却变成了狗
'''
# bar中含有yield,肯定是一个生成器,所以直接@bar("satori", 16)是不会输出的。当我执行foo的时候,还会先执行bar里面yield上面的内容,
# 然后执行foo代码的内容,最后执行yield下面的内容,并且此时yield后面的内容是什么也已经无关紧要了,因为根本用不到了
关闭打开的句柄
诸如打开文件之类的io操作,都会有一个close操作。因此为了确保关闭,可以使用contextlib中的一个叫做closing的类
import contextlib
class Door:
def __init__(self):
print("__init__()")
self.status = "open"
def close(self):
print("close()")
self.status = "closed"
with contextlib.closing(Door()) as door:
print("此时门的状态:", door.status)
"""
__init__()
此时门的状态: open
close()
"""
print("最后门的状态:", door.status) # 最后门的状态: closed
"""
contextlib.closing接收类的实例对象,其实主要就帮我们做了两件事
一个是可以通过with语句的方式来执行,另一个是执行完毕之后自动帮我们调用close方法
"""
我们还是看看源码如何实现的
class closing(AbstractContextManager):
def __init__(self, thing):
# 这里的thing显然是我们之前传入的Door的实例对象door
self.thing = thing
def __enter__(self):
# 先调用__enter__返回之前的实例
return self.thing
def __exit__(self, *exc_info):
# 最后调用我们实例的close方法
# 而且我们发现__enter__返回的是我们定义的类的实例
# 这也再次证明了调用__exit__跟__enter__返回的是什么没有任何关系
# 这里是由closing实例对象调用的
self.thing.close()
import contextlib
class Door:
def __init__(self):
print("__init__()")
self.status = "open"
def close(self):
print("close()")
self.status = "closed"
# 如果出现了异常怎么办呢?不用怕,依旧会执行close语句.
# 由于contextlib.closing的__exit__函数并没有返回布尔类型为True的值,所以最后还是会抛出异常,我们手动捕获一下
try:
with contextlib.closing(Door()) as boy_next_door:
print(123)
1 / 0
print(456)
except Exception:
pass
print(boy_next_door.status)
'''
__init__()
123
close()
closed
'''
# 最后还是打印了"closed",所以还是执行了close()方法
忽略异常
很多情况下,忽略库产生的异常很有用,因为这个错误可能会显示期望的状态已经被实现,否则该错误就可以被忽略。 要忽略异常,最常用的办法就是利用一个try except语句。但是在我们此刻的主题中,try except也可以被替换成contextlib.suppress(),以更显示地抑制with块中产生的异常
import contextlib
def foo():
print(123)
1 / 0
print(456)
with contextlib.suppress(ZeroDivisionError):
foo()
print(789)
'''
123
'''
# 最终只输出了123,可以看到不仅1/0下面的456没有被打印,连foo()下面的789也没有被打印
# 可以传入多个异常
with contextlib.suppress(ZeroDivisionError, BaseException, Exception):
foo()
'''
123
'''
# 出现异常之后,会将异常全部丢弃
# 如果出现的异常没有在suppress里面指定,那么是要报错的
重定向到输出流
import contextlib
import io
import sys
'''
我们可以用redirect_stdout和redirect_stderr上下文管理器从这些函数中捕获输出
'''
def func(a):
sys.stdout.write(f"stdout :{a}") # 等价于print(f"stdout :{a}"),不指定file默认是往sys.stdout也就是控制台输出的
sys.stderr.write(f"stderr :{a}") # 等价于print(f"stdout :{a}", file=sys.stderr)
capture = io.StringIO()
'''
我们执行func本来是要往sys.stdout和sys.stderr里面写的
但这是在with语句contextlib.redirect_stdout(capture), contextlib.redirect_stderr(capture)下面,
因此可以理解往sys.stdout和sys.stderr里面写的内容就被捕获到了,然后会将捕获到的内容输入到capture里面,因为我们指定了capture
'''
with contextlib.redirect_stdout(capture), contextlib.redirect_stderr(capture):
func("蛤蛤蛤蛤")
print(capture.getvalue()) # stdout :蛤蛤蛤蛤stderr :蛤蛤蛤蛤
'''
redirect_stdout和redirect_stderr会修改全局状态,替换sys模块中的对象,可以想象gevent里面的patch_all会将Python里面socket,ssl等都换掉。
因此要使用这两个函数,必须要注意。这些函数并不保证线程安全,所以在多线程应用中调用这些函数可能会有不确定的结果。
如果有其他希望标准输出流关联到终端设备,那么redirect_stdout和redirect_stderr将会干扰和影响那些操作。
'''
当然这个例子让我想起了golang里面的接口,我们发现上面的capture,指定了是io.StringIO,那么除了io.StringIO还可以指定别的吗?当然可以,只要实现了write方法的对象都可以。
import contextlib
import sys
def func(a):
sys.stdout.write(f"stdout :{a}")
sys.stderr.write(f"stderr :{a}")
with open("1.txt", "w", encoding="utf-8") as f:
with contextlib.redirect_stdout(f), contextlib.redirect_stderr(f):
func("蛤蛤蛤蛤")
显然文件句柄是支持write方法的
动态上下文管理器栈
import contextlib
'''
大多数上下文管理器都一次处理一个对象,如单个文件或数据库句柄。
在这些情况下,对象是提前已知的,并且使用上下文管理器的代码可以建立这一对象上。
另外一些情况下,程序可能需要在一个上下文中创建未知数目的对象,控制流退出这个上下文时所有这些对象都要清理,ExitStack就是用来处理这些更动态的情况。
ExitStack实例会维护清理回调的一个栈数据结构,这些回调显示地填充在上下文中,在控制流退出上下文时会以逆序调用所有注册的回调。
结果类似于有多个嵌套的with语句,只不过它们是动态建立的。
'''
# 可以使用多种方法填充ExitStack,比如
@contextlib.contextmanager
def make_context(i):
print(f"{i}: entering")
yield {i}
print(f"{i}: exiting")
def variable_stack(n, msg):
with contextlib.ExitStack() as stack:
for i in range(n):
d = stack.enter_context(make_context(i))
print(d)
print(msg)
variable_stack(2, "inside stack")
# 输出结果
''''
0: entering
{0}
1: entering
{1}
inside stack
1: exiting
0: exiting
'''
'''
contextlib.ExitStack()相当于创建了上下文管理器栈
stack.enter_context将上下文管理器放入到栈中,注意此时已经执行了
等于是把yield之后的结果压入栈中,stack.enter_context的返回值就是yield后面的值
会先输出:
0: entering
{0}
1: entering
{1}
然后执行下面的代码,所以会打印出msg
当里面的代码执行完毕之后,会继续执行栈里面的数据,但是栈是后入先出的。i=1后入栈,所以先执行
所以最后输出:
1: exiting
0: exiting
'''