是否总是可能将实现内容管理器的类转换为使用contextmanager装饰器的函数?

时间:2018-08-12 20:56:02

标签: python contextmanager

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

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

现在,我想产生相同的功能,但是我不想使用类,而是想使用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')

我在做什么错?是否可以用由contextmanager装饰器装饰的功能实现上下文管理器的类实现我的工作?

主要问题:

是否可以将实现上下文管理器协议的任何类转换为使用contextmanager装饰器的函数?每个选项的局限性是什么?有没有一种情况比另一种更好?

1 个答案:

答案 0 :(得分:1)

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

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

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


您需要进行一些更改,以使返回的内容不是Indenter对象,而是另一个对indenter的调用。尽管这是可能的(任何类都可以重写为闭包),但这可能不是您想要的。

如果您愿意稍微更改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不会创建上下文管理器,但是会创建一个返回上下文管理器的函数。这是@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附加到结果上,然后{{1 }},但这不是主要问题),问题在于yield要求生成器函数产生一次,并在每次调用时提供一次使用的上下文管理器。如果您阅读了contextlib的源代码,那么您将看到如何编写类似contextmanager的代码,而该代码却采用了永久产生交替进入和退出并为您提供执行{{1} } contextmanagernext中的每个}。或者,您可以编写一个在__enter__而不是__exit__上创建生成器的类,以便它可以像用作装饰器(而不是装饰器)一样正确地执行__enter__上下文管理器。但是到那时,为了避免编写一个类,您要编写两个类和一个装饰器,这似乎有点愚蠢。


如果您对更多内容感兴趣,请查看contextlib2,这是由Nick Coghlan和stdlib __init__的其他作者编写的第三方模块。它既用于将_recreate_cm功能向后移植到旧版本的Python,又用于试验新功能以用于将来的Python版本。 IIRC曾经有一个contextlib版本可以重用,但由于无法彻底解决该错误而将其删除。