介绍

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. Linux下的C的开发之GCC的初级使用

    <span style="font-family: Arial, Helvetica, sans-serif; "><span style="white ...

  2. memcached-slab内存管理

    一.Memcache内存分配机制 关于这个机制网上有很多解释的,我个人的总结如下. Page为内存分配的最小单位. Memcached 的内存分配以page为单位,默认情况下一个page是1M,可以通 ...

  3. Centos 7.2 安装与配置JDK8

    系统环境:centos7安装方式:rpm安装软件:jdk-8u25-linux-x64.rpm下载地址:http://www.oracle.com/technetwork/java/javase/do ...

  4. 线程回调,线程中的队列,事件,greenlet模块,gevent模块,自定义补丁, 单线程实现并发,协程

    1.线程回调 在线程池/进程池每次提交任务,都会返回一个表示任务的对象,Future对象Future对象具备一个绑定方法,add_done_callback 用于指定回调函数 add 意味着可以添加多 ...

  5. 关于JavaScript的词法作用域及变量提升的个人理解

    关于JavaScript的作用域,最近听到一个名词:“词法作用域”:以前没有听说过(读书少),记录一下对此的理解,加深印象. 词法作用域:在JavaScript中,一个函数的作用域,在这个函数定义好的 ...

  6. scrapy生成json中文为ASCII码解决

    修改pipelines.py 即可 修改内容如下: import json import codecs import os class BaiduNewsPipeline(object): #增加的 ...

  7. C# Excel 中设置文字对齐方式、方向和换行

    在Excel表格中输入文字时,我们常常需要调整文字对齐方式或者对文字进行换行.本文将介绍如何通过编程的方式设置文字对齐方式,改变文字方向以及对文字进行换行. //创建Workbook对象 Workbo ...

  8. PAT A1012 Best Rank(25)

    题目描述 To evaluate the performance of our first year CS majored students, we consider their grades of ...

  9. E.XKC's basketball team(The Preliminary Contest for ICPC Asia Xuzhou 2019)

    https://nanti.jisuanke.com/t/41387 解: 离散化+线段树. #define IOS ios_base::sync_with_stdio(0); cin.tie(0); ...

  10. centerOS7安装lnmp环境

    视频地址: https://www.bilibili.com/video/av55251610?p=65 安装nginx http://nginx.org 点击 download vim /etc/y ...