【问题标题】:Alternative to contextlib.nested with variable number of context managers使用可变数量的上下文管理器替代 contextlib.nested
【发布时间】:2013-04-11 14:49:02
【问题描述】:

我们的代码根据运行时参数调用可变数量的上下文管理器:

from contextlib import nested, contextmanager

@contextmanager
def my_context(arg):
    print("entering", arg)
    try:
        yield arg
    finally:
        print("exiting", arg)

def my_fn(items): 
    with nested(*(my_context(arg) for arg in items)) as managers:
        print("processing under", managers)

my_fn(range(3))

但是,contextlib.nested is deprecated since Python 2.7:

DeprecationWarning: With-statements now directly support multiple context managers

Multiple variables in Python 'with' statement 的答案表明 contextlib.nested 有一些“令人困惑的容易出错的怪癖”,但是使用多管理器 with 语句的建议替代方法不适用于可变数量的上下文管理器(和也破坏了向后兼容性)。

是否有任何 contextlib.nested 的替代品未被弃用并且(最好)没有相同的错误?

还是我应该继续使用contextlib.nested 并忽略警告?如果是这样,我是否应该计划在将来的某个时间删除contextlib.nested

【问题讨论】:

标签: python deprecated with-statement contextmanager


【解决方案1】:

添加了新的Python 3 contextlib.ExitStack class 以替代contextlib.nested()(请参阅issue 13585)。

它的编码方式让您可以直接在 Python 2 中使用它:

import sys
from collections import deque


class ExitStack(object):
    """Context manager for dynamic management of a stack of exit callbacks

    For example:

        with ExitStack() as stack:
            files = [stack.enter_context(open(fname)) for fname in filenames]
            # All opened files will automatically be closed at the end of
            # the with statement, even if attempts to open files later
            # in the list raise an exception

    """
    def __init__(self):
        self._exit_callbacks = deque()

    def pop_all(self):
        """Preserve the context stack by transferring it to a new instance"""
        new_stack = type(self)()
        new_stack._exit_callbacks = self._exit_callbacks
        self._exit_callbacks = deque()
        return new_stack

    def _push_cm_exit(self, cm, cm_exit):
        """Helper to correctly register callbacks to __exit__ methods"""
        def _exit_wrapper(*exc_details):
            return cm_exit(cm, *exc_details)
        _exit_wrapper.__self__ = cm
        self.push(_exit_wrapper)

    def push(self, exit):
        """Registers a callback with the standard __exit__ method signature

        Can suppress exceptions the same way __exit__ methods can.

        Also accepts any object with an __exit__ method (registering a call
        to the method instead of the object itself)
        """
        # We use an unbound method rather than a bound method to follow
        # the standard lookup behaviour for special methods
        _cb_type = type(exit)
        try:
            exit_method = _cb_type.__exit__
        except AttributeError:
            # Not a context manager, so assume its a callable
            self._exit_callbacks.append(exit)
        else:
            self._push_cm_exit(exit, exit_method)
        return exit # Allow use as a decorator

    def callback(self, callback, *args, **kwds):
        """Registers an arbitrary callback and arguments.

        Cannot suppress exceptions.
        """
        def _exit_wrapper(exc_type, exc, tb):
            callback(*args, **kwds)
        # We changed the signature, so using @wraps is not appropriate, but
        # setting __wrapped__ may still help with introspection
        _exit_wrapper.__wrapped__ = callback
        self.push(_exit_wrapper)
        return callback # Allow use as a decorator

    def enter_context(self, cm):
        """Enters the supplied context manager

        If successful, also pushes its __exit__ method as a callback and
        returns the result of the __enter__ method.
        """
        # We look up the special methods on the type to match the with statement
        _cm_type = type(cm)
        _exit = _cm_type.__exit__
        result = _cm_type.__enter__(cm)
        self._push_cm_exit(cm, _exit)
        return result

    def close(self):
        """Immediately unwind the context stack"""
        self.__exit__(None, None, None)

    def __enter__(self):
        return self

    def __exit__(self, *exc_details):
        # We manipulate the exception state so it behaves as though
        # we were actually nesting multiple with statements
        frame_exc = sys.exc_info()[1]
        def _fix_exception_context(new_exc, old_exc):
            while 1:
                exc_context = new_exc.__context__
                if exc_context in (None, frame_exc):
                    break
                new_exc = exc_context
            new_exc.__context__ = old_exc

        # Callbacks are invoked in LIFO order to match the behaviour of
        # nested context managers
        suppressed_exc = False
        while self._exit_callbacks:
            cb = self._exit_callbacks.pop()
            try:
                if cb(*exc_details):
                    suppressed_exc = True
                    exc_details = (None, None, None)
            except:
                new_exc_details = sys.exc_info()
                # simulate the stack of exceptions by setting the context
                _fix_exception_context(new_exc_details[1], exc_details[1])
                if not self._exit_callbacks:
                    raise
                exc_details = new_exc_details
        return suppressed_exc

使用它作为你的上下文管理器,然后随意添加嵌套上下文管理器:

with ExitStack() as stack:
    managers = [stack.enter_context(my_context(arg)) for arg in items]
    print("processing under", managers)

对于您的示例上下文管理器,这将打印:

>>> my_fn(range(3))
('entering', 0)
('entering', 1)
('entering', 2)
('processing under', [0, 1, 2])
('exiting', 2)
('exiting', 1)
('exiting', 0)

您也可以安装contextlib2 module;它包括 ExitStack 作为后向端口。

【讨论】:

  • 此解决方案仅适用于将要使用的上下文管理器“输入”在与它们组合的同一上下文中的情况。以this code为例,返回nested的结果,供调用者稍后输入。我没有看到在 Python 3 中实现这一点的方法。
  • 您的意思是您想在函数或类似函数中构造一个堆叠的 CM,然后稍后with 语句中使用该堆栈?您也可以使用ExitStack() 来做到这一点;只需在变量中创建一个实例,使用.enter_context() 在其上推送上下文管理器,然后稍后with 语句中使用该实例。您可以像传递其他 Python 对象一样传递对象。
  • 如果你也想推迟进入,一个接受队列并在__enter__() 上调用.enter_context() 的子类(并在发生异常时正确处理退出)'太难写了。
  • 您需要注意的一件事是创建 CM 也会引发异常;因此将可调用对象传递到该队列可能是最好的方法,推迟创建 CM,直到调用 __enter__
  • 我想知道为什么他们没有将其添加到最新版本的 Python 2.7 中,因为 nested 已被弃用,ExitStack 的代码可直接在 Python 2.7 中使用。只要我想支持 Python 2.7,你认为把它复制到我的项目中是我最好的选择吗?
【解决方案2】:

python3 维护者选择破坏向后兼容性有点令人烦恼,因为根据ExitStack 实现nested 非常简单:

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

    @contextmanager
    def nested(*contexts):
        """
        Reimplementation of nested in python 3.
        """
        with ExitStack() as stack:
            for ctx in contexts:
                stack.enter_context(ctx)
            yield contexts

【讨论】:

  • 这个实现和原来的nested有同样的问题。在调用 nested 函数时,上下文管理器已经创建。如果索引 > 0 的上下文管理器的构造函数抛出异常,则前面的上下文管理器将永远不会退出。
  • 确实有这个“问题”,是的。这对于在构造时获取资源的上下文是一个问题(不幸的是,正如open 所做的那样)。有许多上下文不能以这种方式发挥作用,nested 允许您返回单个返回值,该值可用作所有子上下文的上下文。
【解决方案3】:
import sys
import contextlib

class nodeA(object):

    def __init__(self):
        print( '__init__ nodeA')

    def __enter__(self):
        print( '__enter__ nodeA')

    def __exit__(self, a, b, c):
        print( '__exit__ nodeA')

class nodeB(object):

    def __init__(self):
        print( '__init__ nodeB')

    def __enter__(self):
        print( '__enter__ nodeB')

    def __exit__(self, a, b, c):
        print( '__exit__ nodeB')

class nodeC(object):

    def __init__(self):
        print( '__init__ nodeC')

    def __enter__(self):
        print( '__enter__ nodeC')

    def __exit__(self, a, b, c):
        print( '__exit__ nodeC')

print( 'Start...')

a = nodeA()
b = nodeB()
c = nodeC()

print( 'Python version: %s' % (sys.version))

if sys.version.startswith('2'):
    print('Use python 2!')
    with contextlib.nested(a, b, c):
        print('hallo?')

if sys.version.startswith('3'):
    print('Use python 3!')
    with contextlib.ExitStack() as stack:
        [stack.enter_context(arg) for arg in [a,b,c]]

print('...end!')

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-20
    • 1970-01-01
    • 1970-01-01
    • 2011-07-02
    • 2018-02-05
    • 1970-01-01
    相关资源
    最近更新 更多