是否总是可以将实现内容管理器的 class 转换为使用上下文管理器装饰器的函数?

Is it always possible to convert a class that implements the content manager into function that uses contextmanager decorator?

我有以下 class 实现上下文管理器协议:

class Indenter:
    def __init__(self):
        self.level = 0

    def __enter__(self):
        self.level += 1
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.level -= 1

    def print(self, text):
        print('\t' * self.level + text)

以下代码:

with Indenter() as indent:
    indent.print('bye!')
    with indent:
        indent.print('goodbye')
        with indent:
            indent.print('au revoir')
    indent.print('bye bye')

产生以下输出:

    bye!
        goodbye
            au revoir
    bye bye

现在,我想产生相同的功能,但我不想实现 class,而是想使用 contextmanager 装饰器。到目前为止我有以下代码:

class Indenter:
    def __init__(self):
        self.level = 0

    def print(self, text):
        print('\t' * self.level + text)


@contextmanager
def indenter():
    try:
        i = Indenter()
        i.level += 1
        yield i
    finally:
        i.level -= 1

但是,当我调用时无法产生相同的输出:

with indenter() as indent:
    indent.print('hi!')
    with indent:
        indent.print('hello')
        with indent:
            indent.print('bonjour')
    indent.print('hey')

我做错了什么?是否有可能实现我正在使用 class 实现的功能,该 class 通过 contextmanager 装饰器装饰的功能实现上下文管理器?

主要问题:

是否可以将任何实现上下文管理器协议的 class 转换为使用 contextmanager 装饰器的函数?每个选项的限制是什么?是否存在其中一种优于另一种的情况?

你不能做你想做的事,至少不能直接做。

您的 Indenter.__enter__ return 是一个 Indenter 对象。然后,您的嵌套 with indent: 使用那个 Indenter 对象作为上下文管理器——这很好,因为它是一个。

您的 indenter 函数生成一个 Indenter 对象。然后,您的嵌套 with indent: 使用那个 Indenter 对象作为上下文管理器——这失败了,因为它不是一个。


您需要进行一些更改,以便您 return 不是 Indenter 对象,而是对 indenter 的另一个调用。虽然这是可能的(任何 class 都可以重写为闭包),但它可能不是您想要的。

如果您愿意稍微更改 API,您可以这样做:

@contextmanager
def indenter():
    level=0
    @contextmanager
    def _indenter():
        nonlocal level
        try:
            level += 1
            yield
        finally:
            level -= 1
    def _print(text):
        print('\t' * level + text)
    _indenter.print = _print
    yield _indenter

现在,indenter 不会创建上下文管理器,但会创建一个 return 作为上下文管理器的函数。这是 @contextmanager 装饰器所固有的——就像你必须做 with indenter() as indent: 而不是 with indenter as indent: 一样,你必须做 with indent(): 而不是 with indent .

否则,一切都非常简单。我没有使用递归,而是创建了一个将 level 存储在闭包中的新函数。然后我们可以 contextmanager 它并添加 print 方法。现在:

>>> with indenter() as indent:
...     indent.print('hi!')
...     with indent():
...         indent.print('hello')
...         with indent():
...             indent.print('bonjour')
...     indent.print('hey')
hi!
    hello
        bonjour
hey

如果您想知道为什么我们不能只 yield _indenter()(好吧,我们必须调用 _indenter(),然后将 print 添加到结果中,然后 yield 那个,但这不是主要问题),问题是 contextmanager 需要一个产生一次的生成器函数,并在你每次调用它时给你一个一次性的上下文管理器。如果您阅读 contextlib 源代码,您会看到如何编写类似 contextmanager 的代码,它取而代之的是一个函数,该函数永远交替进入和退出,并为您提供一个执行 [=38= 的上下文管理器] 每个 __enter____exit__。或者你可以写一个 class 来在 __enter__ 而不是 __init__ 上创建生成器,这样它就可以正确地做 _recreate_cm 事情,就像它用作装饰器而不是上下文管理器。但是在那个时候,为了避免写一个 class,你要写两个 classes 和一个装饰器,这看起来有点傻。


如果您对更多感兴趣,您应该查看 contextlib2,这是一个由 Nick Coghlan 和 stdlib contextlib 的其他作者编写的第三方模块。它既用于将 contextlib 功能向后移植到 Python 的旧版本,也用于试验 Python 未来版本的新功能。 IIRC,在某一时刻,他们有一个可重复使用的 @contextmanager 版本,但由于一个无法彻底解决的错误而将其删除。