我有以下实现上下文管理器协议的类:
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
装饰器的函数?每个选项的局限性是什么?有没有一种情况比另一种更好?
答案 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} } contextmanager
和next
中的每个}。或者,您可以编写一个在__enter__
而不是__exit__
上创建生成器的类,以便它可以像用作装饰器(而不是装饰器)一样正确地执行__enter__
上下文管理器。但是到那时,为了避免编写一个类,您要编写两个类和一个装饰器,这似乎有点愚蠢。
如果您对更多内容感兴趣,请查看contextlib2
,这是由Nick Coghlan和stdlib __init__
的其他作者编写的第三方模块。它既用于将_recreate_cm
功能向后移植到旧版本的Python,又用于试验新功能以用于将来的Python版本。 IIRC曾经有一个contextlib
版本可以重用,但由于无法彻底解决该错误而将其删除。