yokon's blog

从contextlib源码谈with语句

2018.08.13

上一篇文章中,解决*RuntimeError: Working outside of application context.*错误,使用手动将应用上下文推入栈中:

ctx = app.app_context()
ctx.push()
print(current_app.name)
ctx.pop()

而 flask 文档中给我们的解决代码是:

with app.app_context():
    print(current_app.name)

它使用了pythonwith语句,使得代码更加简洁。

AppContext 类

with语句的关键是需要实现__enter____exit__类方法,来看一下 flask 的AppContext类的实现:

class AppContext(object):
    ...

    def push(self):
        """Binds the app context to the current context."""
        ...

    def pop(self, exc=_sentinel):
        """Pops the app context."""
        ...

    def __enter__(self):
        self.push()
        return self

    def __exit__(self, exc_type, exc_value, tb):
        self.pop(exc_value)

        if BROKEN_PYPY_CTXMGR_EXIT and exc_type is not None:
            reraise(exc_type, exc_value, tb)

AppContext类的__enter__方法实现了push()入栈,__exit__方法实现了pop()出栈。可以让我们不需要啰嗦的再去写入栈出栈语句。

with语句

怎样自己实现with语句呢?首先需要知道pep0343{:target="_blank"}。

该PEP建议实现了__enter__()__exit__()方法的协议称为上下文管理协议,实现该协议的对象称为上下文管理器。紧跟with关键字之后的表达式是上下文表达式,上下文表达式必须要返回一个上下文管理器。

contextlib

python的标准库提供了contextlib模块来帮助编写__enter____exit__方法。contextlib模块提供了contextmanager装饰器,它接受一个生成器(generator),用yield语句把with ... as var的结果输出出去。这样就可以使用with语句了。看下面例子:

from contextlib import contextmanager

@contextmanager
def opened(filename, mode="r"):
    file = open(filename, mode)
    print('start')
    try:
        yield file
    finally:
        file.close()
        print('end')

with opened("/etc/passwd") as f:
    for line in f:
        print(line.strip())

程序在with语句块的执行前后分别打印了startend。跟踪这个装饰器,看看他的实现源码:

def contextmanager(func):
    @wraps(func)
    def helper(*args, **kwds):
        return _GeneratorContextManager(func, args, kwds)
    return helper

他的内部返回一个类的实例_GeneratorContextManager(func, args, kwds),继续跟踪他:

class _GeneratorContextManager(ContextDecorator):
    """Helper for @contextmanager decorator."""

    def __init__(self, func, args, kwds):
        # 初始化实例,将传入的函数赋值给gen属性,此时函数未执行
        self.gen = func(*args, **kwds)
        # 依然是赋值
        self.func, self.args, self.kwds = func, args, kwds

    def _recreate_cm(self):
        # 调用装饰器时都必须重新创建实例
        return self.__class__(self.func, self.args, self.kwds)

    def __enter__(self):
        # __enter__()方法
        try:
            # next()函数是返回一次生成器的值,
            # self.gen是传给contextmanager装饰器传给该类的生成器函数哦,
            # 看上面的__init__()初始化实例方法
            return next(self.gen)
        except StopIteration:
            # 如果生成器没有更多的元素返回时,会抛出StopIteration的错误。
            raise RuntimeError("generator didn't yield") from None

    def __exit__(self, type, value, traceback):
        # __exit__()方法,这几个参数是什么,看下面
        if type is None:
            try:
                next(self.gen)
            except StopIteration:
                return
            else:
                raise RuntimeError("generator didn't stop")
        else:
            if value is None:
                value = type()
            try:
                self.gen.throw(type, value, traceback)
                raise RuntimeError("generator didn't stop after throw()")
            except StopIteration as exc:
                return exc is not value
            except RuntimeError as exc:
                if exc.__cause__ is value:
                    return False
                raise
            except:
                if sys.exc_info()[1] is not value:
                    raise

class ContextDecorator(object):
    "A base class or mixin that enables context managers to work as decorators."

    def __call__(self, func):
        @wraps(func)
        def inner(*args, **kwds):
            with self._recreate_cm():
                return func(*args, **kwds)
        return inner

_GeneratorContextManager(func, args, kwds)的源码很简单明了,看上面的注释基本上可以理解。该类实现了__enter__()__exit__()方法,也就是个上下文管理器

__enter__()__exit__()这两个方法其实很好理解,前者还是在with语句块执行前调用,后者实在语句块执行后调用。

__enter__()

上面的例子中,with语句as关键词后面的f,是什么。如果在with语句块中使用next(f)调用他,他仍然可以正确的输出一行内容。这说明f其实就是file = open()的生成器。他是如何被赋值的。似乎只有在_GeneratorContextManager()类的__enter__()方法中,可以看到他返回了一个next(self.gen),注意这里他并没有被执行。他返回的结果会被赋值给as关键词后面的变量。以一个例子来演示:

class Demo(object):
    def hello(self, name):
        print('hello, ', name)

    def __enter__(self):
        print('start')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('end')

with Demo() as h:
    h.hello('world')

执行结果:

start
hello,  world
end

__exit__()

从上文的几个例子中可以看到,__exit__()方法接收除self外三个参数。从_GeneratorContextManager()类的源码,可以看出他似乎是做的一些上下文管理器的错误处理。仍然以上面的例子,分别打印三个参数来演示:

class Demo(object):
    def hello(self, name):
        print('hello, ', name)

    def __enter__(self):
        print('start')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('end')
        print('type: ', exc_type)
        print('val: ', exc_val)
        print('tb: ', exc_tb)

with Demo() as h:
    1/0
    h.hello('world')

执行结果:

Traceback (most recent call last):
  File "C:/Users/Administrator/Desktop/Eager/spider.py", line 71, in <module>
    1/0
ZeroDivisionError: division by zero
start
end
type:  <class 'ZeroDivisionError'>
val:  division by zero
tb:  <traceback object at 0x0000000000E34C88>

可以看到,程序报了ZeroDivisionError的错误,再看各个参数的返回的值。他们对比一下不难发现:

  • exc_type: 异常类型
  • exc_val: 异常信息
  • exc_tb: 异常追踪信息

_GeneratorContextManager()类中的__exit__()方法的一些判断语句是不是很容易理解了。

其实__exit__()方法还可以使用return语句,返回False或者True可以对with语句块的错误进行抛出或者隐藏。

参考文章:

廖雪峰的python教程{:target="_blank"}

PEP343{:target="_blank"}