保证退出其上下文管理器的协程
Coroutine that is guaranteed to exit its context managers
我想在协程中使用上下文管理器。这个协程应该处理未知数量的步骤。但是,由于步骤数未知,上下文管理器何时退出尚不清楚。我希望它在协程超出范围/被垃圾收集时退出;然而,在下面的例子中这似乎并没有发生:
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
---!
- 上下文管理器从未打印“ENDED”。
我怎样才能制作一个支持任意数量步骤的协程,并保证优雅地退出?我不想让这成为来电者的责任。
你可以通过发送一些会导致它跳出循环的东西来告诉协程关闭,return如下图所示。这样做会导致在完成此操作的地方引发 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
---!
TLDR:所以问题是当在 with
块内引发(而不是处理)异常时。调用上下文管理器的 __exit__
方法时出现该异常。对于 contextmanager
修饰的生成器,这会导致生成器的异常为 throw
n。 cm
不处理此异常,因此清理代码不是 运行。当 coro
被垃圾回收时,它的 close
方法被调用,该方法 throw
从 GeneratorExit
到 coro
(然后被扔到 cm
)。下面是对以上步骤的详细说明。
close
方法 throw
是一个 GeneratorExit
到 coro
,这意味着 GeneratorExit
在 yield
点被引发。 coro
不处理 GeneratorExit
因此它通过错误退出上下文。这会导致使用错误和错误信息调用上下文的 __exit__
方法。 contextmanager
装饰生成器中的 __exit__
method 有什么作用?如果调用它时出现异常,它会将异常抛给底层生成器。
在这一点上,a GeneratorExit
是从上下文管理器主体中的 yield
语句引发的。该未处理的异常导致清理代码不是 运行。该未处理的异常由上下文管理器引发,并传递回 contextmanager
装饰器的 __exit__
。作为抛出的相同错误,__exit__
returns False
表示发送到 __exit__
的原始错误未得到处理。
最后,GeneratorExit
在 coro
内的 with
块之外继续传播,在那里它继续未处理。 但是,不处理GeneratorExits
对于生成器来说是正常的,所以原来的close
方法抑制了GeneratorExit
.
看到这部分yield
documentation:
If the generator is not resumed before it is finalized (by reaching a zero reference count or by being garbage collected), the generator-iterator’s close() method will be called, allowing any pending finally clauses to execute.
查看 close
documentation 我们看到:
Raises a GeneratorExit at the point where the generator function was paused. If the generator function then exits gracefully, is already closed, or raises GeneratorExit (by not catching the exception), close returns to its caller.
这部分with
statement documentation:
The suite is executed.
The context manager’s exit() method is invoked. If an exception caused the suite to be exited, its type, value, and traceback are passed as arguments to exit(). Otherwise, three None arguments are supplied.
以及 contextmanager
装饰器的 __exit__
method 代码。
因此,对于所有这些上下文 (rim-shot),我们可以获得所需行为的最简单方法是在上下文管理器的定义中使用 try-except-finally。这是 contextlib
docs 推荐的方法。他们所有的例子都遵循这种形式。
Thus, you can use a try…except…finally statement to trap the error (if any), or ensure that some cleanup takes place.
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
随心所欲。
我们也可以用传统方式定义我们的上下文管理器:作为一个 class 和一个 __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__
愉快地 运行 发送其清理代码,然后 return 发送 False
。这不是 strictly 必需的,否则 None
(另一个 Falsey 值)将被 returned,但它表明发送到 [=14] 的任何异常=] 没有处理。 (如果没有异常,__exit__
的 return 值无关紧要)。
我想在协程中使用上下文管理器。这个协程应该处理未知数量的步骤。但是,由于步骤数未知,上下文管理器何时退出尚不清楚。我希望它在协程超出范围/被垃圾收集时退出;然而,在下面的例子中这似乎并没有发生:
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
---!
- 上下文管理器从未打印“ENDED”。
我怎样才能制作一个支持任意数量步骤的协程,并保证优雅地退出?我不想让这成为来电者的责任。
你可以通过发送一些会导致它跳出循环的东西来告诉协程关闭,return如下图所示。这样做会导致在完成此操作的地方引发 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
---!
TLDR:所以问题是当在 with
块内引发(而不是处理)异常时。调用上下文管理器的 __exit__
方法时出现该异常。对于 contextmanager
修饰的生成器,这会导致生成器的异常为 throw
n。 cm
不处理此异常,因此清理代码不是 运行。当 coro
被垃圾回收时,它的 close
方法被调用,该方法 throw
从 GeneratorExit
到 coro
(然后被扔到 cm
)。下面是对以上步骤的详细说明。
close
方法 throw
是一个 GeneratorExit
到 coro
,这意味着 GeneratorExit
在 yield
点被引发。 coro
不处理 GeneratorExit
因此它通过错误退出上下文。这会导致使用错误和错误信息调用上下文的 __exit__
方法。 contextmanager
装饰生成器中的 __exit__
method 有什么作用?如果调用它时出现异常,它会将异常抛给底层生成器。
在这一点上,a GeneratorExit
是从上下文管理器主体中的 yield
语句引发的。该未处理的异常导致清理代码不是 运行。该未处理的异常由上下文管理器引发,并传递回 contextmanager
装饰器的 __exit__
。作为抛出的相同错误,__exit__
returns False
表示发送到 __exit__
的原始错误未得到处理。
最后,GeneratorExit
在 coro
内的 with
块之外继续传播,在那里它继续未处理。 但是,不处理GeneratorExits
对于生成器来说是正常的,所以原来的close
方法抑制了GeneratorExit
.
看到这部分yield
documentation:
If the generator is not resumed before it is finalized (by reaching a zero reference count or by being garbage collected), the generator-iterator’s close() method will be called, allowing any pending finally clauses to execute.
查看 close
documentation 我们看到:
Raises a GeneratorExit at the point where the generator function was paused. If the generator function then exits gracefully, is already closed, or raises GeneratorExit (by not catching the exception), close returns to its caller.
这部分with
statement documentation:
The suite is executed.
The context manager’s exit() method is invoked. If an exception caused the suite to be exited, its type, value, and traceback are passed as arguments to exit(). Otherwise, three None arguments are supplied.
以及 contextmanager
装饰器的 __exit__
method 代码。
因此,对于所有这些上下文 (rim-shot),我们可以获得所需行为的最简单方法是在上下文管理器的定义中使用 try-except-finally。这是 contextlib
docs 推荐的方法。他们所有的例子都遵循这种形式。
Thus, you can use a try…except…finally statement to trap the error (if any), or ensure that some cleanup takes place.
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
随心所欲。
我们也可以用传统方式定义我们的上下文管理器:作为一个 class 和一个 __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__
愉快地 运行 发送其清理代码,然后 return 发送 False
。这不是 strictly 必需的,否则 None
(另一个 Falsey 值)将被 returned,但它表明发送到 [=14] 的任何异常=] 没有处理。 (如果没有异常,__exit__
的 return 值无关紧要)。