是否总是可以将实现内容管理器的 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
版本,但由于一个无法彻底解决的错误而将其删除。
我有以下 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
版本,但由于一个无法彻底解决的错误而将其删除。