• 你真的了解python的with语句吗?通过分析contextlib源码让你彻底掌握with的用法


    楔子

    下面我们来聊一下Python中的上下文管理,Python中的上下文管理我们可以通过with语句实现。在Python中使用with语句最多的情况,莫过于操作文件了,比如我们在打开一个文件的时候会通过类似于with open("test.txt", encoding="utf-8") as f: 这种形式打开,这种方式的好处就在于with语句结束后会自动关闭文件。

    那么with语句的原理是什么呢?我们怎么样才能使用with语句呢?这次我们就全方位的剖析一下,并且在Python的标准库中还有一个模块叫做contextlib,从名字上也能看出来这是一个用于上下文管理的模块。我们后面也会通过分析contextlib的源码,来自己实现一下contextlib的功能。

    上下文管理器API

    上下文管理器(context manager)负责管理一个代码块中的资源,会在进入代码块时创建资源,然后再退出代码后清理这个资源。比如:文件就支持上下文管理器API,可以确保文件读写后关闭文件。

    下面我们就来模拟一下文件的读取

    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 f语句,这里的f就是我return的内容")
            return self
    
        def read(self):
            print(f"文件进行读操作,读取文件:{self.filename!r}, 模式:{self.mode!r}, 编码:{self.encoding!r}")
    
        def __exit__(self, exc_type, exc_val, exc_tb):
            print("__exit__, 我是用来清理资源的,当操作执行完毕之后就会执行我,比如:关闭文件")
    
    
    with Open("test.txt", encoding="utf-8") as f:
        f.read()
    """
    __enter__, 有了这个就可以使用with Open() as f语句,这里的f就是我return的内容
    文件进行读操作,读取文件:'test.txt', 模式:'r', 编码:'utf-8'
    __exit__, 我是用来清理资源的,当操作执行完毕之后就会执行我,比如:关闭文件
    """
    

    我们看到with Open() as f:的流程就是,先实例化一个Open对象,然后通过实例对象来调用__enter__方法,将其返回值赋值给with语句中的f,然后执行with语句块中的代码,最后执行__exit__方法。

    但是注意的是:with Open() as f中的这个f是什么,取决于__enter__中返回了什么。

    class Open:
    
        def __enter__(self):
            return "古明地觉"
    
        def __exit__(self, exc_type, exc_val, exc_tb):
            pass
    
    
    # 如果是f = Open(), 那么毫无疑问这个f就是类Open的实例对象
    # 但是对于with Open() as f, 我们说这个f到底是什么, 它取决于__enter__中返回了什么
    with Open() as f:
        print(f)
    """
    古明地觉
    """
    
    # 我们看到print(f)打印的是一个字符串,因为__enter__中返回的就是一个字符串
    # 首先with Open() as f:这一行代码所做的事情就是先实例化一个Open对象
    # 只不过这个实例对象我们没有用变量进行接收, 但它确实存在
    # 然后该实例对象再调用__enter__, 将__enter__的返回值赋值给f
    # 因此__enter__返回了什么, 这个f就是什么, 所以在with代码块中打印f得到的是一个字符串
    # 所以要记住: f是由__enter__的返回值决定的,只不过大多数情况下,__enter__里面返回的都是self本身,所以相应的f指向的也是该类的实例对象
    # 然后with语句结束,也是通过这个实例对象来调用__exit__,这一点需要记清楚
    
    
    # 当然,我们先实例化一个对象,再使用with也是可以的
    o = Open()
    with o as f:
        print(f"{f}, 世界第一可爱")
    """
    古明地觉, 世界第一可爱
    """
    
    # 如果是with Open() as f:, 那么实例化和调用__enter__就放在一起执行了
    # 如果直接对一个实例对象使用with语句
    # 比如with o as f:, 那么会直接通过实例对象o来调用__enter__, 将其返回值赋值给f
    # 当with语句结束,再通过实例对象o来调用__exit__,进行资源的释放等操作
    
    
    # 当然直接with o:, 不通过as f接收__enter__的返回值也是可以的
    with o:
        pass
    

    因此我们看到,一个对象究竟能否使用with语句,取决于实例化该对象的类(或者继承的基类)中是否同时实现了__enter__和__exit__两个魔法函数,两者缺一不可。

    因此with语句的流程我们就很清晰了,以with XXX() as xx:为例,总共分为三步:

    • 创建XXX()的实例对象,然后调用__enter__方法,将其返回值交给as xx中的xx
    • 执行with语句块的代码
    • 最后由该实例对象再调用__exit__进行一些收尾工作。

    __enter__我们清楚了,但是我们发现__exit__里面除了self之外,还有三个参数分别是exc_type, exc_val, exc_tb,它们是做什么的呢?显然这三个参数分别是异常类型、异常值、异常的回溯栈, 从名字上也能看出来。

    class Open:
    
        def __enter__(self):
            return "古明地觉"
    
        def __exit__(self, exc_type, exc_val, exc_tb):
            # 这几个参数是使用pycharm的时候,自动帮我们加上去的
            print("__exit__执行:")
            print(exc_type)
            print(exc_val)
            print(exc_tb)
            return True
    
    
    with Open() as f:
        print(f)
    """
    古明地觉
    __exit__执行:
    None
    None
    None
    """
    
    # 我们看到exc_type, exc_val, exc_tb三者全部为None
    # 因为它们是和异常有关的,而我们这里没有出现异常,所以为None
    # 但如果出现异常了呢?
    with Open() as f:
        print(f)
        1 / 0
        print(123)
        print(456)
        print(789)
    print("你猜我会被执行吗?")
    """
    古明地觉
    __exit__执行:
    <class 'ZeroDivisionError'>
    division by zero
    <traceback object at 0x0000024CD4E4C080>
    你猜我会被执行吗?
    """
    

    我们看到在没有出现异常的时候,exc_type, exc_val, exc_tb打印的值全部是None。然而一旦with语句里面出现了异常,那么会立即执行__exit__,并将 异常的类型,异常的值,异常的回溯栈 传入到__exit__中。

    因此:当with语句正常结束之后会调用__exit__,如果with语句里面出现了异常则会立即调用__exit__。

    但是__exit__函数返回了个True是什么意思呢?当with语句里面出现了异常,理论上是会报错的,但是由于要执行__exit__函数,所以相当于暂时把异常塞进了嘴里。

    如果__exit__函数最后返回了一个布尔类型为True的值,那么会把塞进嘴里的异常吞下去,程序不报错正常执行。如果返回布尔类型为False的值,会在执行完__exit__函数之后再把异常吐出来,引发程序崩溃。

    这里我们返回了True,因此程序正常执行,最后一句话被打印了出来。但是 1 / 0 这句代码后面的几个print却没有打印,为什么呢?

    因为我们说上下文管理执行是有顺序的,1) 先实例化Open的实例对象,调用__enter__方法,将__enter__的返回值交给f,2) 执行with语句块的代码,3) 最后调用__exit__

    只要__exit__函数执行结束,那么这个with语句就算结束了。而with语句里面如果有异常,那么会立即进入__exit__函数,因此异常语句后面的代码是无论如何都不会被执行的。另外,如果__enter__中出现了异常,那么with语句会直接报错,同理__exit__中如果出现了异常也会报错,即使最后返回了True。

    目前我们就把with说完了,下面我们进行contextlib的分析。我们说contextlib是一个专门用于上下文管理的内置模块,我们就来分析一下它内部是怎么实现的。

    contextlib实现机制的分析

    上下文管理器作为函数修饰符

    contextlib中有一个类ContextDecorator,增加了对常规上下文管理器类的支持,使得上下文管理器,也可以作为函数的装饰器,我们来看一下。

    import contextlib
    
    
    class Context(contextlib.ContextDecorator):
        def __init__(self, how_used):
            self.how_used = 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})")
            return True
    
    
    @Context("我要去装饰了")
    def foo(name):
        print(123)
        return f"我的名字叫: {name}"
    
    
    print(foo("古明地觉"))
    """
    __enter__(我要去装饰了)
    123
    __exit__(我要去装饰了)
    我的名字叫: 古明地觉
    """
    

    我们看到上下文管理器还可以作为函数的装饰器,我们看到先执行了__enter__,然后执行foo函数内部的代码,最后执行__exit__,打印返回值。

    我们分析一下内部是如何实现的,首先我们装饰foo的时候,显然是使用Context的实例对象去装饰的,相当于给这个实例对象加上了括号,并且把foo这个函数作为参数传进去了。既然实例对象加上了括号(调用),这就意味着该实例对象对应的类一定有__call__方法,但是我们定义的没有,那么继承的父类肯定有。

    ContextDecorator这个类的代码很少,我们直接仿照一个。

    from functools import wraps
    
    
    class MyContextDecorator:
    
        def __call__(self, func):
            @wraps(func)
            def inner(*args, **kwargs):
                with self:
                    return func(*args, **kwargs)
            return inner
    
    
    class Context(MyContextDecorator):
        def __init__(self, how_used):
            self.how_used = 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})")
            return True
    
    
    @Context("我要去装饰了")
    def foo(name):
        print(123)
        return f"我的名字叫: {name}"
    
    
    print(foo("古明地觉"))
    """
    __enter__(我要去装饰了)
    123
    __exit__(我要去装饰了)
    我的名字叫: 古明地觉
    """
    

    我们实现了一个类MyContextDecorator,实现了一模一样的效果。下面就来分析一下,整个流程。

    class MyContextDecorator:
    
        def __call__(self, func):
            @wraps(func)
            def inner(*args, **kwargs):
                with self:
                    return func(*args, **kwargs)
            return inner
    

    类的源码很少,当我们使用实例对象去装饰foo函数的时候,就相当于给实例对象加上了括号

    而类Context内部没有定义__call__, 那么肯定要走Context的父类也就是这里的__call__。

    但是执行的时候,里面这个self可不是MyContextDecorator的实例对象,而是我们之前定义的Context类里面的self,也就是Context的实例对象。

    如果面向对象基础不好的话,建议去复习一下,这里简单说一下。调用Context类实例对象的时候,肯定先去找Context里面的__call__方法,但是没有,那么会调用父类的,但是调用时对应的self还是Context的self

    因此之前的@Context("我要作为装饰器去装饰"),就等价于context = Context("我要作为装饰器去装饰"); @context

    然后再装饰foo的时候,相当于foo = context(foo),那么会调用这里的__call__方法,然后foo会被传递给这里的func,然后返回inner。

    所以foo在被装饰完之后,foo就变成了这里的inner,只不过由于有@wraps(func)这个装饰器在,所以装饰之后的函数名、__doc__等元信息没有改变

    那么当我再调用foo("古明地觉")的时候,就等价于调用这里的inner("古明地觉"),而里面的with self:中的self,显然就是Context的实例对象。

    所以就很清晰了,with self:,会先执行Context中的__enter__,然后执行func、也就是我们原本的foo,拿到它的返回值,然后执行__exit__,最后最外层的print再将拿到的返回值打印。

    整体的逻辑就是上面分析的那样,希望能仔细理清一遍这里的流程。但是有一点需要注意:外层的print打印的是foo的返回值,不是__exit__的返回值。

    @Context("我要去装饰了")
    def foo(name):
        print(123)
        1 / 0
        return f"我的名字叫: {name}"
    
    
    print(foo("古明地觉"))
    """
    __enter__(我要去装饰了)
    123
    __exit__(我要去装饰了)
    None
    """
    

    我们看到foo中出现了异常,导致立刻执行了__exit__,而返回值则是一个None,因为foo执行失败了,所以结果就是一个None,不是__exit__里面的True。

    从生成器到上下文管理器

    采用传统方式创建上下文管理器并不难,只需要包含一个__enter__方法和一个__exit__方法的类即可。 不过某些时候,如果只有很少的上下文需要管理,那么完整地写出所以代码便会成为额外的负担。 在这些情况下,可以使用contextmanager修饰符将一个生成器函数转换为上下文管理器。

    import contextlib
    
    
    @contextlib.contextmanager
    def foo(name, where):
        print(f"我的名字是: {name}, 居住在: {where}")
        yield "baka⑨"
        print(f"只要你喜欢{name}, 我们就是好兄弟")
    
    
    with foo("古明地觉", "东方地灵殿") as f:
        print(f.upper())
    """
    我的名字是: 古明地觉, 居住在: 东方地灵殿
    BAKA⑨
    只要你喜欢古明地觉, 我们就是好兄弟
    """
    

    只要给函数加上这个装饰器,那么便可以使用with语句。当中的yield相当于将代码块分隔为两个战场:yield上面的代码相当于__enter__会先执行,然后将yield的值交给f,然后执行with语句,最后执行yield下面的代码块,相当于__exit__

    注意:如果使用contextmanager装饰的话,函数中只能出现、且必须出现一个yield。

    下面我们来手动实现一下contextmanager函数,contextlib中实现的比较复杂,主要是最后对异常进行了很多的检测。我们可以进行适当地简化一下,把主要的逻辑实现一下。

    from functools import wraps
    
    
    def my_contextmanager(func):
    
        class GeneratorContextManager:
    
            def __init__(self, func, *args, **kwargs):
                self.gen = func(*args, **kwargs)
    
            def __enter__(self):
                try:
                    assert hasattr(self.gen, "__next__")
                    return next(self.gen)
                except AssertionError:
                    raise RuntimeError("函数中必须出现、且只能出现一个yield") from None
    
            def __exit__(self, exc_type, exc_val, exc_tb):
                try:
                    next(self.gen)
                except StopIteration:
                    return False
                else:
                    raise RuntimeError("函数中必须出现、且只能出现一个yield")
    
        @wraps(func)
        def inner(*args, **kwargs):
            return GeneratorContextManager(func, *args, **kwargs)
        return inner
    
    
    @my_contextmanager
    def foo(name, where):
        print(f"我的名字是: {name}, 居住在: {where}")
        yield "baka⑨"
        print(f"只要你喜欢{name}, 我们就是好兄弟")
    
    
    with foo("古明地觉", "东方地灵殿") as f:
        print(f.upper())
    """
    我的名字是: 古明地觉, 居住在: 东方地灵殿
    BAKA⑨
    只要你喜欢古明地觉, 我们就是好兄弟
    """
    

    我们手动的实现了一个contextmanager,下面还是分析一下整体的流程。

    def my_contextmanager(func):
    
        class GeneratorContextManager:
    
            def __init__(self, func, *args, **kwargs):
                self.gen = func(*args, **kwargs)
    
            def __enter__(self):
                try:
                    assert hasattr(self.gen, "__next__")
                    return next(self.gen)
                except AssertionError:
                    raise RuntimeError("函数中必须出现、且只能出现一个yield") from None
    
            def __exit__(self, exc_type, exc_val, exc_tb):
                try:
                    next(self.gen)
                except StopIteration:
                    return False
                else:
                    raise RuntimeError("函数中必须出现、且只能出现一个yield")
    
        @wraps(func)
        def inner(*args, **kwargs):
            return GeneratorContextManager(func, *args, **kwargs)
        return inner
    

    当使用my_contextmanager装饰的时候,外层的foo函数就变成了这里的inner。

    然后with foo("古明地觉", "东方地灵殿") as f:的时候,等价于with inner("古明地觉", "东方地灵殿") as f:,等价于with GeneratorContextManager(foo, "古明地觉", "东方地灵殿") as f:

    然后创建一个GeneratorContextManager的实例对象,而self.gen显然是生成器函数foo对应的生成器,此时生成器已经创建。

    我们创建完实例对象之后,要干啥来着,要执行__enter__。而我们说函数中必须出现yield,那么它是一个生成器函数,所以self.gen内部要有__next__方法,因此进行一个断言。

    然后return next(value)开始驱动生成器运行了,当运行到yield的时候停下来。此时yield上面的代码已经执行完毕了,然后返回yield后面的值,赋值给f,开始执行with里面的代码。

    with语句块的代码执行完毕,执行__exit__,在里面我们继续next,显然要驱动生成器继续执行,将yield下面的代码执行完毕,如果出现了StopIteration,那么什么也不做。如果没有出现此异常,证明不止一个yield,那么就抛出一个RuntimeError

    所以我们看到,这个装饰器本质上还是使用了类的上下文管理。

    由于contextmanager返回的上下文管理器派生自ContextDecorator,所以也可以被用作函数修饰符

    @contextlib.contextmanager
    def deco(name, where):
        print(f"我的名字是: {name}, 居住在: {where}")
        yield
        print(f"只要你喜欢{name}, 我们就是好兄弟")
    
    
    @deco("古明地觉", "东方地灵殿")
    def bar():
        print("猜猜我会在什么地方输出")
    
    bar()
    """
    我的名字是: 古明地觉, 居住在: 东方地灵殿
    猜猜我会在什么地方输出
    只要你喜欢古明地觉, 我们就是好兄弟
    """
    

    当我执行bar的时候,还会先执行deco中yield上面的内容,然后执行bar代码的内容,最后执行deco中yield下面的内容。并且此时yield后面的内容是什么也已经无关紧要了,因为根本用不到了,yield不出去了。

    那么我们如何实现上面的功能呢?可以当做练习自己尝试一下,并不难。

    关闭打开的句柄

    诸如打开文件之类的io操作,都会有一个close操作。因此为了确保关闭,可以使用contextlib中的一个叫做closing的类

    class Open:
    
        def __init__(self):
            self.status = "open"
    
        def main(self):
            return "执行了很复杂的逻辑"
    
        def close(self):
            self.status = "closed"
    
    
    with contextlib.closing(Open()) as f:
        print(f.main())  # 执行了很复杂的逻辑
        print(f"状态: {f.status}")  # 状态: open
    
    # with语句结束后
    print(f"状态: {f.status}")  # 状态: closed
    

    contextlib.closing接收类的实例对象,其实主要就帮我们做了两件事:一个是可以通过with语句的方式来执行,另一个是执行完毕之后自动帮我们调用close方法。这里我们不手动实现了,非常简单,感觉没啥卵用,直接看源码中是如何实现的吧。

    class closing(AbstractContextManager):
    
        def __init__(self, thing):
            # 这里的thing显然是我们之前传入的Open的实例对象f
            self.thing = thing
        def __enter__(self):
            # 先调用__enter__返回之前的实例
            return self.thing
        def __exit__(self, *exc_info):
            # 最后调用我们实例的close方法
            self.thing.close()
    

    忽略异常

    很多情况下,忽略产生的异常很有用,如果这个异常无法百分百避免、并且该异常又没啥卵用,那么该错误就可以被忽略。 要忽略异常,最常用的办法就是利用一个try except语句。但是在我们此刻的主题中,try except也可以被替换成contextlib.suppress(),以更显示地抑制with块中产生的异常

    def foo():
        print(123)
        1 / 0
        print(456)
    
    
    with contextlib.suppress(ZeroDivisionError, TypeError):
        foo()
        print(789)
    """
    123
    """
    

    在foo中出现了除零错误,但是程序并没有报错。相当于异常被"镇压"了,注意:如果with块中出现的异常,无法被suppress传入的异常捕获的话,那么异常还是会抛出来的。但是对于当前的例子来说,除零错误显然是被成功捕获了,最终只输出了123,可以看到不仅1/0下面的456没有被打印,连foo()下面的789也没有被打印。

    因为只要出现了异常,就会进入到__exit__中,我们看一下源码是如何实现的。

    class suppress(AbstractContextManager):
        def __init__(self, *exceptions):
            self._exceptions = exceptions
    
        def __enter__(self):
            pass
    
        def __exit__(self, exctype, excinst, exctb):
            return exctype is not None and issubclass(exctype, self._exceptions)
    

    非常简单,没有发生异常就不说了,如果发生异常,但它是指定的某个异常的子类的话,程序也不会报错。注意:一个类也是其本身的子类,issubclass(TypeError, TypeError)结果为True。

    异步上下文管理器的实现

    Python在3.5的时候引入了async和await,可以通过async def定义一个原生的协程函数,通过await驱动一个协程执行。

    而异步上下文则可以通过async with来实现

    import asyncio
    
    
    class A:
    
        def __init__(self, name):
            self.name = name
    
        async def __aenter__(self):
            print("__aenter__")
            return self
    
        async def __aexit__(self, exc_type, exc_val, exc_tb):
            print("__aexit__")
            return True
    
    
    # 必须定义一个协程函数,然后事件循环驱动协程执行
    async def main():
        async with A("古明地觉") as f:
            print("<<<>>>")
    
    
    asyncio.run(main())
    """
    __aenter__
    <<<>>>
    __aexit__
    """
    

    当然contextlib这个包里面也实现与异步上下文管理相关的类和函数。

    import contextlib
    import asyncio
    
    
    @contextlib.asynccontextmanager
    async def foo():
        print(123)
        yield 456
        print(789)
    
    
    async def main():
        async with foo() as f:
            print(f == 456)
    
    
    asyncio.run(main())
    """
    123
    True
    789
    """
    

    关于异步上下文管理,其实和普通的同步上下文管理是类似的,有兴趣可以自己实现一下。

  • 相关阅读:
    337粘贴与选择性粘贴
    Excel VBA定制开发如何将数据重复指定次数听语音
    317、单元格的Offset
    Shape.Type属性名称及对应值列表
    Mysql解决The total number of locks exceeds the lock table size错误
    python QT5 tableview 用法1
    python qt5 菜单栏一级菜单响应单击事件
    自定义类加载器
    SpringCloudConfig与Nacos多个领域配置的不同
    多线程工具ForkJoinPool
  • 原文地址:https://www.cnblogs.com/traditional/p/11487979.html
Copyright © 2020-2023  润新知