我想在协同线中使用上下文管理器。这个协同线应该处理数目未知的步骤。但是,由于步骤数量未知,不清楚上下文管理器何时应该退出。当协同例程超出范围/垃圾收集时,我希望它退出;但是,在下面的示例中,这种情况似乎不会发生:
import contextlib
@contextlib.contextmanager
def cm():
print("STARTED")
yield
print("ENDED")
def coro(a: str):
with cm():
print(a)
while True:
val1, val2 = yield
print(val1, val2)
c = coro("HI")
c.send(None)
print("---")
c.send((1, 2))
print("---!")此程序的输出:
STARTED
HI
---
1 2
---!上下文管理器
我如何才能建立一个协同机制来支持任意数量的步骤,并确保它优雅地退出?我不想让这成为来电者的责任。
发布于 2021-08-20 15:39:19
TLDR:所以问题是当在with块中引发(而不是处理)异常时。该异常将调用上下文管理器的__exit__方法。对于contextmanager-decorated生成器,这将导致异常为生成器的thrown。cm不处理此异常,因此不运行清理代码。当coro被垃圾收集时,它的close方法被调用哪个throw是GeneratorExit到coro (然后抛出到cm)。下面是对上述步骤的详细说明。
close方法throw是从GeneratorExit到coro的GeneratorExit,这意味着GeneratorExit是在yield的点引发的。coro不处理GeneratorExit,因此它通过错误退出上下文。这将导致使用错误和错误信息调用上下文的__exit__方法。来自contextmanager-decorated生成器的-decorated是做什么的?如果使用异常调用它,则会将该异常抛给基础生成器。
此时,从上下文管理器正文中的GeneratorExit语句引发a yield。未处理的异常将导致清除代码不运行。未处理的异常由上下文管理器引发,并传递回contextmanager装饰器的contextmanager。由于抛出的错误与抛出的相同,__exit__返回False以指示发送给__exit__的原始错误未处理。
最后,这将继续GeneratorExit在coro内部的with块之外的传播,在那里它仍然未被处理。然而,对于生成器来说,不处理GeneratorExits是正常的,所以原始的close方法会抑制GeneratorExit。
请参阅yield documentation的这一部分
如果生成器在最后确定之前未恢复(通过达到零引用计数或被垃圾收集),则将调用生成器-迭代器的close()方法,允许执行任何挂起的
子句。
看看我们看到的close documentation:
在生成器函数暂停时引发GeneratorExit。如果生成器函数然后优雅地退出,已经关闭,或引发GeneratorExit (通过不捕获异常),则关闭返回给调用方。
with statement documentation的这一部分
以及用于__exit__ method装饰器的contextmanager代码。
因此,对于所有这些上下文(rim-shot),我们能够获得期望行为的最简单的方法是尝试-除了-最后在上下文管理器的定义中。这是来自contextlib docs的建议方法。他们所有的例子都是这样的。
因此,您可以使用
…除…外语句来捕获错误(如果有的话),或者确保进行某种清理。
import contextlib
@contextlib.contextmanager
def cm():
try:
print("STARTED")
yield
except Exception:
raise
finally:
print("ENDED")
def coro(a: str):
with cm():
print(a)
while True:
val1, val2 = yield
print(val1, val2)
c = coro("HI")
c.send(None)
print("---")
c.send((1, 2))
print("---!")现在的产出是:
STARTED
HI
---
1 2
---!
ENDED如你所愿。
我们还可以用传统的方式定义上下文管理器:作为一个具有__enter__和__exit__方法的类,并且仍然得到正确的行为:
class CM:
def __enter__(self):
print('STARTED')
def __exit__(self, exc_type, exc_value, traceback):
print('ENDED')
return False情况稍微简单一些,因为我们可以准确地看到__exit__方法是什么,而不必去看源代码。GeneratorExit被发送到__exit__ (作为参数),__exit__在那里愉快地运行其清理代码,然后返回False。这并不是绝对必要的,因为否则将返回None (另一个Falsey值),但它表示没有处理发送给__exit__的任何异常。(如果没有例外,__exit__的返回值并不重要)。
发布于 2021-08-19 16:08:07
您可以通过告诉协同线关闭,通过发送一些会导致它脱离循环并返回的东西来做到这一点,如下所示。这样做将导致在执行此操作时引发StopIteration异常,因此我添加了另一个上下文管理器以允许对其进行抑制。注意,我还添加了一个coroutine装饰器,使它们在第一次调用时自动启动,但该部分严格来说是可选的。
import contextlib
from typing import Callable
QUIT = 'quit'
def coroutine(func: Callable):
""" Decorator to make coroutines automatically start when called. """
def start(*args, **kwargs):
cr = func(*args, **kwargs)
next(cr)
return cr
return start
@contextlib.contextmanager
def ignored(*exceptions):
try:
yield
except exceptions:
pass
@contextlib.contextmanager
def cm():
print("STARTED")
yield
print("ENDED")
@coroutine
def coro(a: str):
with cm():
print(a)
while True:
value = (yield)
if value == QUIT:
break
val1, val2 = value
print(val1, val2)
print("---")
with ignored(StopIteration):
c = coro("HI")
#c.send(None) # No longer needed.
c.send((1, 2))
c.send((3, 5))
c.send(QUIT) # Tell coroutine to clean itself up and exit.
print("---!")输出:
STARTED
HI
---
1 2
3 5
ENDED
---!https://stackoverflow.com/questions/68850560
复制相似问题