contextlib:上下文管理器工具

介绍

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方法的

1

动态上下文管理器栈

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

猜你喜欢

转载自www.cnblogs.com/traditional/p/11874389.html