Python 具有可变数量上下文管理器的contextlib.nested的替代方案
我们的代码根据运行时参数调用不同数量的上下文管理器:Python 具有可变数量上下文管理器的contextlib.nested的替代方案,python,deprecated,with-statement,contextmanager,Python,Deprecated,With Statement,Contextmanager,我们的代码根据运行时参数调用不同数量的上下文管理器: 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_c
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
有一些“容易混淆的错误怪癖”,但建议的使用多个管理器with
语句的替代方案对数量可变的上下文管理器不起作用(也破坏了向后兼容性)
除了contextlib.nested
之外,还有其他没有被弃用并且(最好)没有相同bug的方法吗
或者我应该继续使用contextlib.nested
并忽略警告吗?如果是这样,我是否应该计划在将来某个时候删除contextlib.nested
?新添加的内容是作为contextlib.nested()
的替换内容(请参阅)
它的编码方式可以直接在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)
您也可以安装;它包括
ExitStack
作为一个后端口。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
关于任意数量的经理,你提出了一个很好的观点。。。在我看来,这一点应该在邮件列表的某个地方提出……这就是为什么引入Python 3的原因。请看,此解决方案仅适用于将要使用的上下文管理器“输入”在其组装的相同上下文中的情况。例如,在中,
nested
的结果将返回给调用方,以便调用方稍后输入。我看不到在Python 3中实现这一点的方法。您的意思是希望在函数或类似函数中构造一个堆栈CM,然后在with
语句中使用该堆栈?您也可以使用ExitStack()
来实现这一点;只需在变量中创建一个实例,使用。输入_context()
在其上推送上下文管理器,然后在with
语句中使用该实例。您可以像其他Python对象一样传递该对象。如果您想推迟输入,也可以使用一个子类,该子类接受队列并调用。在上输入上下文()
(并在发生异常时正确处理退出)你需要注意的一点是,创建CM也会引发异常;因此,将可调用项传递到该队列可能是最好的方法,将CMs的创建推迟到调用\uuuu enter\uuuu
为止。我想知道为什么他们没有将此添加到Python 2.7的最新版本中,因为nested
已经被弃用,而ExitStack
的代码在Python 2.7中可以直接使用。你认为只要我想支持Python2.7,就把它复制到我的项目中是最好的选择吗?这个实现与原始的nested
有相同的问题。调用nested
函数时,上下文管理器已经创建。如果索引>0的上下文管理器的构造函数引发异常,则不会退出前面的上下文管理器。它确实存在该“问题”,是的。这仅仅是在构建资源时抓住资源的上下文的问题(不幸的是,open
就是这样)。有许多上下文不是这样工作的,嵌套的允许您返回单个返回值,该值可以用作所有子上下文的上下文。
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
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!')