介绍

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

contextlib:上下文管理器工具的更多相关文章

  1. (转)contextlib — 上下文管理器工具

    原文:https://pythoncaff.com/docs/pymotw/contextlib-context-manager-tool/95 这是一篇社区协同翻译的文章,你可以点击右边区块信息里的 ...

  2. python contextlib 上下文管理器

    1.with操作符 在python中读写文件,可能需要这样的代码 try-finally读写文件 file_text = None try: file_text = open('./text', 'r ...

  3. contextlib 上下文管理器

    在Python中,读写文件这样的资源要特别注意,必须在使用完毕后正确关闭它们.正确关闭文件资源的一个方法是使用try...finally: try: f = open('/path/to/file', ...

  4. python 黑魔法 ---上下文管理器(contextor)

    所谓上下文 计算机上下文(Context)对于我而言,一直是一个很抽象的名词.就像形而上一样,经常听见有人说,但是无法和现实认知世界相结合. 最直观的上下文,莫过于小学的语文课,经常会问联系上下文,推 ...

  5. python上下文管理器ContextLib及with语句

    http://blog.csdn.net/pipisorry/article/details/50444736 with语句 with语句是从 Python 2.5 开始引入的一种与异常处理相关的功能 ...

  6. Python 上下文管理器模块--contextlib

    在 Python 处理文件的时候我们使用 with 关键词来进行文件的资源关闭,但是并不是只有文件操作才能使用 with 语句.今天就让我们一起学习 Python 中的上下文管理 contextlib ...

  7. 【Python】 上下文管理器和contextlib

    上下文管理器 一直对python中的上下文管理比较迷惑,趁着今天研究SQLAlchemy顺便看了一下,感觉稍微清楚了一点.http://www.cnblogs.com/chenny7/p/421344 ...

  8. python 上下文管理器contextlib.ContextManager

    1 模块简介 在数年前,Python 2.5 加入了一个非常特殊的关键字,就是with.with语句允许开发者创建上下文管理器.什么是上下文管理器?上下文管理器就是允许你可以自动地开始和结束一些事情. ...

  9. Python中的上下文管理器(contextlib模块)

    上下文管理器的任务是:代码块执行前准备,代码块执行后收拾 1 如何使用上下文管理器: 打开一个文件,并写入"hello world" filename="my.txt&q ...

随机推荐

  1. Ubuntu配置zsh

    安装zsh sudo apt-get install zsh chsh -s /bin/zsh 配置 curl -L https://raw.github.com/robbyrussell/oh-my ...

  2. 1. hadoop使用启动命令时报错之分析解决

    今天在学习hadoop启动命令的时候,先jps看了下,发现namenode.datanode都开着,所以想要先停止这些服务,结果输入命令后报错:“WARN util.NativeCodeLoader: ...

  3. TTL 传输中过期,内部网络环路

    ping目标地址的时候,如果不是显示超时,而是很快出现TTL 传输中过期,很可能情况是内部网络出现环路 tracert一下目标地址,如果路由不断重复,说明是环路

  4. Git(2):基本操作

    Git 创建仓库 执行<git init>命令后,Git仓库会生成一个.git目录,该目录包含了资源的所有元数据,其他的项目目录保持不变(Git 只在仓库的根目录生成 .git 目录). ...

  5. spring-boot集成6:集成redis实现字典缓存功能

    Why redis? redis是基于内存的key-value系统,可以用作缓存中间件或者消息中间件,spring-boot提供了方便的方式和redis集成. 1.maven依赖 <!--red ...

  6. GIL全局解释器锁,线程池与进程池 同步异步,阻塞与非阻塞,异步回调

    GIL全局解释器锁 1.什么是GIL 官方解释:'''In CPython, the global interpreter lock, or GIL, is a mutex that prevents ...

  7. win7 开远程记录

    1,HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server\Wds\rdpwd\Tds\tcp HKEY_LOCAL_M ...

  8. layui 时间选择器 不要秒的选项

    通过修改 CSS 样式可以隐藏秒的选项 ++ .laydate-time-list{padding-bottom:0;overflow:hidden} .laydate-time-list>li ...

  9. new与malloc有什么区别

    转自http://www.cnblogs.com/QG-whz/p/5140930.html 前言 几个星期前去面试C++研发的实习岗位,面试官问了个问题: new与malloc有什么区别? 这是个老 ...

  10. 数位dp详解&&LG P2602 [ZJOI2010]数字计数

    数位dp,适用于解决一类求x~y之间有多少个符合要求的数或者其他. 例题 题目描述 杭州交通管理局经常会扩充一些的士车牌照,新近出来一个好消息,以后上牌照,不再含有不吉利的数字了,这样一来,就可以消除 ...