• 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
    '''
    
  • 相关阅读:
    很好的Socket教程
    TcpClient 错误"不能做任何连接,因为目标机器积极地拒绝它" 的解决
    Tcp通信 暑期学习笔记(二)
    svn1.5+TortoiseSVN1.5+VisualSVN1.5
    进程、线程、应用程序域 暑期学习笔记(一)
    线程状态(转)
    Udp通信 暑期学习笔记(三)
    杜婧/于洋(为奥运冠军名字作诗)
    王峰(为奥运冠军名字作诗)
    刘子歌(为奥运冠军名字作诗)
  • 原文地址:https://www.cnblogs.com/traditional/p/11874389.html
Copyright © 2020-2023  润新知