介绍
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 '''