将 Python 上下文管理器的迭代器嵌套在“with”中

发布于 2025-01-02 14:20:09 字数 171 浏览 3 评论 0原文

我有一个返回上下文管理器的迭代器。

我想要一个 pythonic with 语句,它模拟多个嵌套 with 语句的行为,每个语句对应迭代器返回的每个上下文管理器。

有人可能会说,我想要(已弃用的)contextlib.nested 函数的泛化。

I have an iterator that returns context managers.

I want a pythonic with statement, that emulates the behaviour of several nested with statements, one for each context manager returned by the iterator.

One could say, I want a generalisation of the (deprecated) contextlib.nested function.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

远山浅 2025-01-09 14:20:09

来自 文档

需要支持嵌套可变数量上下文管理器的开发人员可以使用 warnings 模块来抑制 [contextlib.nested< 引发的 DeprecationWarning /code>] 或者使用此函数作为应用程序特定实现的模型。


处理多个上下文管理器的困难在于它们的交互非常重要:例如,您可能__enter__第一个,然后在__enter__第二个中引发异常。这些边缘情况正是导致嵌套被弃用的原因。如果您想支持他们,您必须非常仔细地考虑如何编写代码。您可能希望阅读 PEP-0343 以获取想法。

From the docs:

Developers that need to support nesting of a variable number of context managers can either use the warnings module to suppress the DeprecationWarning raised by [contextlib.nested] or else use this function as a model for an application specific implementation.

The difficult thing about handling multiple context managers is that they interact non-trivially: for example, you might __enter__ the first then raise an exception in __enter__ing the second. These sort of edge cases are precisely what caused nested to be deprecated. If you want to support them, you will have to think very carefully about how you write your code. You may wish to read PEP-0343 for ideas.

梦里兽 2025-01-09 14:20:09

contextlib.nested 有两个主要问题导致它被弃用。

  1. 第一个问题是内部上下文管理器可能会在__init____new__期间引发异常,并且这些异常将导致整个with语句在不调用<外部管理器的code>__exit__。
  2. 第二个问题更复杂。如果内部管理器之一引发异常,并且外部管理器之一通过在 __exit__ 中返回 True 捕获异常,则仍应执行该块。但在nested的实现中,它只是引发一个RuntimeError而不执行该块。这个问题可能需要完全重写nested

但只需删除nested 定义中的一个* 就可以解决第一问题!
这改变了行为,使得 nested 不再接受参数列表(这无论如何都没有用,因为 with 已经可以处理它),而只接受迭代器。因此,我将新版本称为“iter_nested”。
然后,用户可以定义一个迭代器,在迭代期间实例化上下文管理器。

带有生成器的示例:

def contexts():
    yield MyContext1()
    yield MyContext2()

with iter_nested(contexts()) as contexts:
    do_stuff(contexts[0])
    do_other_stuff(contexts[1])

原始代码和我更改后的 nested 版本的代码之间的差异如下:

from contextlib import contextmanager

@contextmanager
--- def nested(*managers):
+++ def iter_nested(mgr_iterator):
    --- #comments & deprecation warning
    exits = []
    vars = []
    --- exc = (None, None, None)
    +++ exc = None # Python 3
    try:
        --- for mgr in managers:
        +++ for mgr in mgr_iterator:
            exit = mgr.__exit__
            enter = mgr.__enter__
            vars.append(enter())
            exits.append(exit)
        yield vars
# All of the following is new and fit for Python 3
except Exception as exception:
    exc = exception
    exc_tuple = (type(exc), exc, exc.__traceback__)
else:
    exc_tuple = (None, None, None)
finally:
    while exits:
        exit = exits.pop()
        try:
            if exit(*exc_tuple):
                exc = None
                exc_tuple = (None, None, None)
        except Exception as exception:
            exception.__context__ = exc
            exc = exception
            exc_tuple = (type(exc), exc, exc.__traceback__)
    if exc:
        raise exc

contextlib.nested has two major problems that caused it to be deprecated.

  1. first problem is that inner context managers might raise Exceptions during __init__ or __new__, and these exceptions would cause the whole with statement to abort without calling __exit__ of the outer manager.
  2. The second problem is more complicated. If one of the inner managers raises an exception and one of the outer managers catches it by returning True in __exit__, the block should still be executed. But in the implementation of nested, it just raises a RuntimeError without executing the block. This problem probably requires a total rewrite of nested.

But it is possible to solve the first problem by just removing one * in the definition of nested!
This changes the behaviour such that nested doesn't accept argument lists anymore (which isn't useful anyway because with can handle that already) but only an iterator. I therefore call the new version "iter_nested".
The user can then define an iterator that instantiates the context managers during iteration.

An example with a generator:

def contexts():
    yield MyContext1()
    yield MyContext2()

with iter_nested(contexts()) as contexts:
    do_stuff(contexts[0])
    do_other_stuff(contexts[1])

The difference between the codes of the original and my changed version of nested is here:

from contextlib import contextmanager

@contextmanager
--- def nested(*managers):
+++ def iter_nested(mgr_iterator):
    --- #comments & deprecation warning
    exits = []
    vars = []
    --- exc = (None, None, None)
    +++ exc = None # Python 3
    try:
        --- for mgr in managers:
        +++ for mgr in mgr_iterator:
            exit = mgr.__exit__
            enter = mgr.__enter__
            vars.append(enter())
            exits.append(exit)
        yield vars
# All of the following is new and fit for Python 3
except Exception as exception:
    exc = exception
    exc_tuple = (type(exc), exc, exc.__traceback__)
else:
    exc_tuple = (None, None, None)
finally:
    while exits:
        exit = exits.pop()
        try:
            if exit(*exc_tuple):
                exc = None
                exc_tuple = (None, None, None)
        except Exception as exception:
            exception.__context__ = exc
            exc = exception
            exc_tuple = (type(exc), exc, exc.__traceback__)
    if exc:
        raise exc
聊慰 2025-01-09 14:20:09

古老的问题,但我偶然发现了它。现在的一种解决方案是使用 contextlib.ExitStack()

with contextlib.ExitStack() as exit_stack:
    for cm in my_context_manager_iterator:
        exit_stack.enter_context(cm)
    # we are now within those contexts... do more things

它处理其他答案中提到的问题。如果输入一个上下文管理器失败,您已输入的其他上下文管理器将正确退出。

对于异步使用,还有 AsyncExitStackenter_async_context()

Ancient question but I stumbled upon it. One solution now is to use contextlib.ExitStack():

with contextlib.ExitStack() as exit_stack:
    for cm in my_context_manager_iterator:
        exit_stack.enter_context(cm)
    # we are now within those contexts... do more things

It handles the problems mentioned in other answers. If entering one context manager fails, the others you have already entered will be exited properly.

For async usage there is also AsyncExitStack with enter_async_context()

执手闯天涯 2025-01-09 14:20:09

此实现 - 或或多或少类似的东西,应该执行后期 contextçlib.nested 过去所做的事情,但如果在进入新上下文时引发异常,则要处理已输入的上下文。

上下文可以作为上下文协议对象或元组传递给它,
其中第一个成员是被调用对象,在托管环境中将使用元组的其余部分作为参数进行调用:

import sys
import traceback


class NestContext(object):
    def __init__(self, *objects):
        self.objects = objects
    def __enter__(self):
        self.contexts = []
        for obj in self.objects:
            if isinstance(obj, tuple):
                try:
                    obj = obj[0](*obj[1:])
                except Exception, error:
                    self.__exit__(type(error), error, sys.exc_info()[2])
                    raise
            try:
                context = obj.__enter__()
            except Exception, error:
                self.__exit__(type(error), error, sys.exc_info()[2])
                raise   
            self.contexts.append(context)
        return self

    def __iter__(self):
        for context in self.contexts:
            yield context

    def __exit__(self, *args):
        for context in reversed(self.contexts):
            try:
                context.__exit__(*args)
            except Exception, error:
                sys.stderr.write(str(error))

if __name__ == "__main__":
    # example uasage

    class PlainContext(object):
        counter  = 0
        def __enter__(self):
            self.counter = self.__class__.counter
            print self.counter
            self.__class__.counter += 1
            return self
        def __exit__(self, *args):
            print "exiting %d" % self.counter

    with NestContext(*((PlainContext,) for i in range(10))) as all_contexts:
        print tuple(all_contexts)

This implementation - or something more or less like this, should make what the late contextçlib.nested used to do, but taking care of the already entered contexts if an exception is raised when entering a new context.

Contexts can be passed to it either as a context-protocol object, or as a tuple,
where the first member is a called object that will be called with the remainder of the tuple as parameters, in a managed environment:

import sys
import traceback


class NestContext(object):
    def __init__(self, *objects):
        self.objects = objects
    def __enter__(self):
        self.contexts = []
        for obj in self.objects:
            if isinstance(obj, tuple):
                try:
                    obj = obj[0](*obj[1:])
                except Exception, error:
                    self.__exit__(type(error), error, sys.exc_info()[2])
                    raise
            try:
                context = obj.__enter__()
            except Exception, error:
                self.__exit__(type(error), error, sys.exc_info()[2])
                raise   
            self.contexts.append(context)
        return self

    def __iter__(self):
        for context in self.contexts:
            yield context

    def __exit__(self, *args):
        for context in reversed(self.contexts):
            try:
                context.__exit__(*args)
            except Exception, error:
                sys.stderr.write(str(error))

if __name__ == "__main__":
    # example uasage

    class PlainContext(object):
        counter  = 0
        def __enter__(self):
            self.counter = self.__class__.counter
            print self.counter
            self.__class__.counter += 1
            return self
        def __exit__(self, *args):
            print "exiting %d" % self.counter

    with NestContext(*((PlainContext,) for i in range(10))) as all_contexts:
        print tuple(all_contexts)
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文