如何在 python 中链接上下文管理器?

How to chain context managers in python?

长话短说,让第二段代码像第一段一样工作的正确方法是什么?

stack_device = [None]
stack_context = [None]

@contextlib.contextmanager
def device(device):
    stack_device.append(device)
    try:
        yield
    finally:
        stack_device.pop()


@contextlib.contextmanager
def context(ctx):
    stack_context.append(ctx)
    try:
        with device("dev"):
            yield
    finally:
        stack_context.pop()


with context("myctx"):
    print(stack_device[-1])  # -> dev
    print(stack_context[-1]) # -> ctx

而那个,当然,在我需要的时候没有正确的设备设置:

stack_device = [None]
stack_context = [None]

class Device():
    def __init__(self, device):
        self.device = device

    def __enter__(self):
        stack_device.append(self.device)
        return

    def __exit__(self, type, value, traceback):
        stack_device.pop()


class Context():
    def __init__(self, ctx):
        self.ctx = ctx

    def __enter__(self):
        with Device("cls_dvc"):
            stack_context.append(self.ctx)
            return

    def __exit__(self, type, value, traceback):
        stack_context.pop()


with Context("myctx"):
    print(stack_device[-1])  # -> None !!!
    print(stack_context[-1]) # -> myctx

在第二种情况下实现与第一种情况相同的行为的正确方法是什么?

我通过将 with Device() 管理器放在 with Context() 中得到正确的输出。

stack_device = [None]
stack_context = [None]

class Device():
    def __init__(self, device):
        self.device = device

    def __enter__(self):
        stack_device.append(self.device)
        return

    def __exit__(self, type, value, traceback):
        stack_device.pop()


class SubContext():
    def __init__(self, ctx):
        self.ctx = ctx

    def __enter__(self):
        stack_context.append(self.ctx)
        return


    def __exit__(self, type, value, traceback):
        stack_context.pop()

class Context:

    def __init__(self, ctx):
        self.ctx = SubContext(ctx)
        self.device = Device('dev')

    def __enter__(self):
        self.ctx.__enter__()
        self.device.__enter__()

    def __exit__(self, type, value, traceback):
        self.ctx.__exit__(type, value, traceback)
        self.device.__exit__(type, value, traceback)

with Context("myctx"):
    print(stack_device[-1])
    print(stack_context[-1])

您需要在 Context class 中创建一个 Device 对象,在 Context __enter__ 方法中调用 Device 对象的 __enter__ 方法,并调用 Device 对象的 __exit__ 方法在 Context __exit__ 方法中。如果出现错误,那么您可以在 Context __exit__ 方法或 Device __exit__ 方法中处理,哪个更合适。

stack_device = [None]
stack_context = [None]

class Device:
    def __init__(self, device):
        self.device = device

    def __enter__(self):
        stack_device.append(self.device)
        return self

    def __exit__(self, err_type, err_value, traceback):
        stack_device.pop()


class Context:
    def __init__(self, ctx):
        self.ctx = ctx
        self.cls_dvc = Device("cls_dvc")

    def __enter__(self):
        self.cls_dvc.__enter__()
        stack_context.append(self.ctx)
        return self

    def __exit__(self, err_type, err_value, traceback):
        stack_context.pop()
        self.cls_dvc.__exit__(err_type, err_value, traceback)


with Context("myctx"):
    print(stack_device[-1])  # -> cls_dvc
    print(stack_context[-1]) # -> myctx

代码失败的关键是 with 语句调用 enter当它的块时,退出当块结束时。在 Context 的 enter 中有一个 with Device 块意味着 return 语句,即使在块内,也会离开它,从而触发 exit 的设备。您可以通过在每个特殊方法中添加打印来查看此操作过程。

有几种可能的解决方案可以使其发挥作用:

  1. 如果上下文管理器是独立的,就像在 Robert Kearns 的解决方案中一样,您可以使用标准 Python 链接从左开始按顺序创建它们,而不是创建一个超级上下文来创建您的两个上下文向右:
    with Context("myctx"), Device("cls_dvc"):
  2. 如果您需要访问 Context class 中的设备属性(按照您的方式链接它们的唯一原因),那么您有两个解决方案:
    • 重新设计您的 class 结构,以便单个 __enter__ 启动两个上下文,单个 __exit__ 清除两个上下文.在你的设计中,只需要将 Context 实现为上下文管理器,Device 可以是常规的 class.
    • 如果你需要将 Device 保持为独立的上下文管理器(例如,到在其他地方单独使用它),那么 Jack Taylor 的解决方案就是您所需要的。请注意,Python 中有几种情况(例如,open("filename"))可以同时用作 "regular" 和 "context managers"。在这种情况下,所有逻辑都在常规方法中,而 __enter____exit__ 仅包含对常规方法的调用方法,因此您不必直接调用特殊方法,如提供的答案