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

5
长话短说,如何让第二段代码与第一段完全相同?
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

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

3个回答

4
你的代码失败的关键是,with语句在其块时调用enter,并在块结束时调用exit。在Context的enter内部放置一个with Device块意味着return语句即使在块内部也会离开它,从而触发Device的exit。您可以通过在每个特殊方法中添加打印来查看此操作过程。
有几种可能的解决方案可使其正常工作:
  1. 如果上下文管理器是独立的,例如在Robert Kearns的解决方案中,您可以使用标准Python链接按顺序创建它们,而不是创建超级上下文来创建两个上下文:
    with Context("myctx"), Device("cls_dvc"):
  2. 如果您需要在Context类内部访问Device属性(以您的方式链接它们的唯一原因),那么您还有两个解决方案:
    • 重新设计类结构,使单个__enter__初始化两个上下文,并且单个__exit__清除两个上下文。在您的设计中,只需实现Context作为上下文管理器,Device可以是常规类。
    • 如果您需要将Device保持为独立的上下文管理器(例如,在其他地方单独使用它),那么Jack Taylor的解决方案就是您需要的。请注意,Python中有几种情况(例如,open(“filename”))可以用作“常规”和“上下文管理器”。在这种情况下,所有逻辑都在常规方法中,而__enter____exit__仅包含对常规方法的调用,因此您无需直接调用特殊方法,如提供的答案所示。

4

您需要在Context类内部创建一个Device对象,在Context的__enter__方法中调用Device对象的__enter__方法,并在Context的__exit__方法中调用Device对象的__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

我原以为__enter__是一个私有成员,但测试表明它是公共的。 - Daniel Chin
@DanielChin Python实际上没有私有成员。即使在属性名称中使用两个前导下划线,如果你了解命名系统,仍然可以从其他对象中访问它们。 - Jack Taylor
1
此外,__enter__ 是一个特殊的魔术方法,因此Python对其进行了特殊处理。与 __foo 这样的属性不同,名称修饰(参见上面链接的文章)并不适用于它。请参阅Python数据模型以获取所有魔术方法和属性的列表。 - Jack Taylor

1

通过将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])

当然可以。但是你的代码并不相同,因为你在使用2个with语句,而代码段1只需使用1个就可以得到相同的输出。如果我正在编写一个库,并希望我的客户只使用我的“上下文(Context)”对象。同时还要求他们将其包装在第三方的“设备(Device)”对象中,那么这并不是一个好的选择,对吧? - y.selivonchyk
我添加了另一种解决方案。我只是随意提出的,这对你有用吗? - Robert Kearns

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接