将两个上下文管理器合二为一

时间:2017-08-09 11:44:05

标签: python contextmanager

我使用Python 2.7,我知道我可以写这个:

with A() as a, B() as b:
    do_something()

我想提供一个方便的帮手,两者兼得。这个助手的用法应如下所示:

with AB() as ab:
    do_something()

现在AB()应该做两件事:创建上下文A()并创建上下文B()。

我不知道如何编写这个便利助手

1 个答案:

答案 0 :(得分:39)

不要重新发明轮子;这并不像看起来那么简单。

上下文管理器被视为堆栈,并且应该以与它们输入相反的顺序退出,例如。如果发生异常,此订单很重要,因为任何上下文管理器都可以抑制异常,此时其余的经理甚至不会收到通知。 __exit__方法也允许引发不同的异常,然后其他上下文管理器应该能够处理该新异常。接下来,成功创建A()表示如果B()因异常而失败,则应通知他。

现在,如果你想要做的就是创建一个你知道的 fixed 数量的上下文管理器,只需在生成器函数上使用@contextlib.contextmanager decorator

from contextlib import contextmanager

@contextmanager
def ab_context():
    with A() as a, B() as b:
        yield (a, b)

然后将其用作:

with ab_context() as ab:

如果您需要处理变量数量的上下文管理器,那么就不要构建自己的实现;请改用标准库contextlib.ExitStack() implementation

from contextlib import ExitStack

with ExitStack() as stack:
    cms = [stack.enter_context(cls()) for cls in (A, B)]

    # ...

然后ExitStack负责正确嵌套上下文管理器,正确处理退出,正确处理和正确传递异常(包括在被抑制时不传递异常,并传递新的提出例外)。

如果您认为两行(with和单独调用enter_context())过于繁琐,您可以使用单独的@contextmanager-decorated generator function

from contextlib import ExitStack, contextmanager

@contextmanager
def multi_context(*cms):
    with ExitStack() as stack:
        yield [stack.enter_context(cls()) for cls in cms]

然后像这样使用ab_context

with multi_context(A, B) as ab:
    # ...

对于Python 2,安装contextlib2 package,并使用以下导入:

try:
    from contextlib import ExitStack, contextmanager
except ImportError:
    # Python 2
    from contextlib2 import ExitStack, contextmanager

这可以让你避免在Python 2上重新发明这个轮子。

无论你做什么,使用contextlib.nested();出于很好的理由,这已经从Python 3中的库中删除了;它也没有正确地实现处理嵌套上下文的进入和退出。