我们可以在同一个 with 语句中的另一个 class 中将 contextmanager 装饰器与 __enter__() 和 __exit__() 方法混合使用吗?

Can we mix contextmanager decorator with __enter__() and __exit__() methods in another class inside the same with statement?

在 python3.8 中,我非常熟悉传统的 __enter____exit__ 魔术方法,但对 @contextlib.contextmanager 装饰器还是陌生的。是否可以在单个 with 语句中混合使用两种模式?

下面的(高度设计的)脚本应该能更清楚地解释问题。是否有 ContextClass.enter_context_function()ContextClass.exit_context_function() 的定义(我想 __init__ 内部也需要更改)仅使用 context_function() 函数并使单元测试通过?还是这些模式相互排斥?

import contextlib


NUMBERS = []


@contextlib.contextmanager
def context_function():
    NUMBERS.append(3)
    try:
        yield
    finally:
        NUMBERS.append(5)


class ContextClass:
    def __init__(self):
        self.numbers = NUMBERS
        self.numbers.append(1)

    def __enter__(self):
        self.numbers.append(2)
        self.enter_context_function() # should append 3
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.exit_context_function() # should append 5
        self.numbers.append(6)

    def function_call(self):
        self.numbers.append(4)

    def enter_context_function(self):
        # FIX ME!
        pass

    def exit_context_function(self):
        # FIX ME!
        pass


if __name__ == "__main__":
    import unittest

    class TestContextManagerFunctionAndClass(unittest.TestCase):
        def test_context_function_and_class(self):
            with ContextClass() as cc:
                cc.function_call()
            self.assertEqual(NUMBERS, [1, 2, 3, 4, 5, 6])

    unittest.main()

我知道有更好的方法可以解决类似的问题(特别是将 context_function 重写为 class 并使用其自己的 __enter____exit__ 方法,但我'我试图更好地理解 contextmanager 装饰器的工作原理。

无需更改 __init__。 “使单元测试通过”的手动方式是:

def enter_context_function(self):
    self._context_mgr = context_function()
    self._context_mgr.__enter__()

def exit_context_function(self):
    self._context_mgr.__exit__(None, None, None)

然而,它有点遗漏了 context-managers 的要点。它们旨在用于 with-statement.

另请注意,如所写,如果屈服后的代码加注,则可能无法达到 NUMBERS.append(5) 行(“拆卸”)。应该这样写:

@contextlib.contextmanager
def context_function():
    NUMBERS.append(3)
    try:
        yield
    finally:
        NUMBERS.append(5)