处理上下文管理器实例在另一个上下文管理器内的情况

43
在Python中如何处理在另一个上下文管理器内创建的上下文管理器?
例如:假设您有一个充当上下文管理器的类A和一个同样充当上下文管理器的类B。但是,类B的实例将必须实例化并使用类A的实例。我已经阅读过PEP 343,并想到了以下解决方案:
class A(object):
    def __enter__(self):
        # Acquire some resources here
        return self

    def __exit__(seplf, exception_type, exception, traceback):
        # Release the resources and clean up
        pass


class B(object):
    def __init__(self):
        self.a = A()

    def __enter__(self):
        # Acquire some resources, but also need to "start" our instance of A
        self.a.__enter__()
        return self

    def __exit__(self, exception_type, exception, traceback):
        # Release the resources, and make our instance of A clean up as well
        self.a.__exit__(exception_type, exception, traceback)

这是正确的方法吗?或者我是否遗漏了一些需要注意的地方?


在评论中,您写道“它是具有递归引用其自身实例的相同类。”您能否提供一个演示此功能的示例或代码摘录?这将有助于制定正确的解决方案。 - Noctis Skytower
4个回答

19

如果您能使用@contextlib.contextmanager装饰器,您的生活将变得更加轻松:

import contextlib

@contextlib.contextmanager
def internal_cm():
    try:
        print "Entering internal_cm"
        yield None
        print "Exiting cleanly from internal_cm"
    finally:
        print "Finally internal_cm"


@contextlib.contextmanager
def external_cm():
    with internal_cm() as c:
        try:
            print "In external_cm_f", c
            yield [c]
            print "Exiting cleanly from external_cm_f", c
        finally:
            print "Finally external_cm_f", c


if "__main__" == __name__:
    with external_cm():
        print "Location A"
    print
    with external_cm():
        print "Location B"
        raise Exception("Some exception occurs!!")

9

或者,您可以这样编写代码:

with A() as a:
    with B(a) as b:
        # your code here

你可能想尝试的另一个解决方案是:

class A:

    def __init__(self):
        pass

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        pass

class B(A):

    def __init__(self):
        super().__init__()

    def __enter__(self):
        super().__enter__()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        super().__exit__(exc_type, exc_val, exc_tb)

在考虑了您的情况说明后,这可能是一个更好的解决方案:
class Resource:

    def __init__(self, dependency=None):
        self.dependency = dependency
        # your code here

    def __enter__(self):
        if self.dependency:
            self.dependency.__enter__()
        # your code here
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        # your code here
        if self.dependency:
            self.dependency.__exit__(exc_type, exc_val, exc_tb)

我不确定下面的实现是否正确,但是__exit__必须妥善处理异常。对于如何在递归调用时正确处理异常,我有些难以想象。

class Resource:

    def __init__(self, dependency=None):
        self.dependency = dependency
        self.my_init()

    def __enter__(self):
        if self.dependency:
            self.dependency.__enter__()
        return self.my_enter()

    def __exit__(self, exc_type, exc_val, exc_tb):
        suppress = False
        try:
            suppress = self.my_exit(exc_type, exc_val, exc_tb)
        except:
            exc_type, exc_val, exc_tb = sys.exc_info()
        if suppress:
            exc_type = exc_val = exc_tb = None
        if self.dependency:
            suppress = self.dependeny.__exit__(exc_type, exc_val, exc_tb)
            if not supress:
                raise exc_val.with_traceback(exc_tb) from None
        return suppress

    def my_init(self):
        pass

    def my_enter(self):
        pass

    def my_exit(self, exc_type, exc_val, exc_tb):
        pass

1
有趣。这也可以行得通。这种方法的缺点是我们的 B 类用户将不得不创建 A 的实例并将其传递给我们。此外,如果需求链超过一级深度,它会变得越来越复杂。但对于简单情况是个好主意。谢谢。 - Sahand
1
@NoctisSkytower,你的基于类的方法只有在B真正是A的子类时才是可行的。在我看来,这种关系不应该仅仅为了更容易地将它们嵌套为上下文管理器而创建,因为它违反了面向对象编程的“IS-A”原则。 - dano
我同意@dano的观点。如果B作为A的子类在逻辑上是有意义的,那么这是一个非常好的解决方案。我上面的例子过于简单化了。在我的实际用例中,同一类别具有对其自身实例的递归引用(有点像链表),因此它们都需要递归释放。继承的想法在那里行不通。 - Sahand
@NoctisSkytower 这为什么很重要?就我特定用例的数据流而言,我更倾向于将B内部的A实例依赖于B的实例,而不是反过来。因此,我很好奇你认为在B之前输入A并在其之后退出很重要的理由是什么。(澄清一下,我并不反对,只是好奇原因。) - Sahand
1
@Sahand 第四个示例尝试尊重__exit__方法的工作方式。返回值必须与方法可能引发的任何异常一起考虑。试图重新实现with语句的机制稍微有些复杂。请参阅PEP 343了解其规格说明。 - Noctis Skytower
显示剩余2条评论

2

这里是在上下文管理器中手动管理资源的示例:外部上下文管理器负责管理内部上下文管理器。

class Inner:

    def __enter__(self):
        print("<inner>")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("</inner>")


class Outer:

    def __init__(self):
        self.inner = Inner()

    def __enter__(self):
        self.inner.__enter__()
        try:
            #raise RuntimeError("Suppose we fail here")
            print("<outer>")
            return self
        except Exception as e:
            self.inner.__exit__(None, None, None)
            raise e

    def __exit__(self, exc_type, exc_value, traceback):
        print("</outer>")
        self.inner.__exit__(exc_type, exc_value, traceback)

使用方法与正常情况下一样:

with Outer() as scope:
    #raise RuntimeError("Suppose we fail here")
    pass

细心的读者会注意到,内部的上下文管理器现在变成了一个无意义的木偶(因为我们手动拉动它的线)。既然如此,那就这样吧。

根据您对异常处理的需求,__exit__可能需要返回self.inner.__exit__(...) - undefined

0

在kuzzooroo的最受欢迎的答案的基础上进行扩展,但将其应用于Python 3+:

为了方便起见,这里是kuzzooroo的原始代码作为Python 3代码(基本上为print语句添加括号:

import contextlib

@contextlib.contextmanager
def internal_cm():
    try:
        print("Entering internal_cm")
        yield None
        print("Exiting cleanly from internal_cm")
    finally:
        print("Finally internal_cm")


@contextlib.contextmanager
def external_cm():
    with internal_cm() as c:
        try:
            print("In external_cm_f")
            yield [c]
            print("Exiting cleanly from external_cm_f")
        finally:
            print("Finally external_cm_f")

if "__main__" == __name__:
    with external_cm():
        print("Location A")
    with external_cm():
        print("Location B")
        raise Exception("Some exception occurs!!")

这是脚本的输出:

Entering internal_cm
In external_cm_f
Location A
Exiting cleanly from external_cm_f
Finally external_cm_f
Exiting cleanly from internal_cm
Finally internal_cm
Entering internal_cm
In external_cm_f
Location B
Finally external_cm_f
Finally internal_cm
Traceback (most recent call last):
  File "main.py", line 28, in <module>
    raise Exception("Some exception occurs!!")
Exception: Some exception occurs!!

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