首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >保证退出上下文管理器的Coroutine

保证退出上下文管理器的Coroutine
EN

Stack Overflow用户
提问于 2021-08-19 15:23:50
回答 2查看 439关注 0票数 1

我想在协同线中使用上下文管理器。这个协同线应该处理数目未知的步骤。但是,由于步骤数量未知,不清楚上下文管理器何时应该退出。当协同例程超出范围/垃圾收集时,我希望它退出;但是,在下面的示例中,这种情况似乎不会发生:

代码语言:javascript
复制
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("---!")

此程序的输出:

代码语言:javascript
复制
STARTED
HI
---
1 2
---!

上下文管理器

  • 从未打印"ENDED".

我如何才能建立一个协同机制来支持任意数量的步骤,并确保它优雅地退出?我不想让这成为来电者的责任。

EN

回答 2

Stack Overflow用户

回答已采纳

发布于 2021-08-20 15:39:19

TLDR:所以问题是当在with块中引发(而不是处理)异常时。该异常将调用上下文管理器的__exit__方法。对于contextmanager-decorated生成器,这将导致异常为生成器的thrown。cm不处理此异常,因此不运行清理代码。当coro被垃圾收集时,它的close方法被调用哪个throwGeneratorExitcoro (然后抛出到cm)。下面是对上述步骤的详细说明。

close方法throw是从GeneratorExitcoroGeneratorExit,这意味着GeneratorExit是在yield的点引发的。coro不处理GeneratorExit,因此它通过错误退出上下文。这将导致使用错误和错误信息调用上下文的__exit__方法。来自contextmanager-decorated生成器的-decorated是做什么的?如果使用异常调用它,则会将该异常抛给基础生成器。

此时,从上下文管理器正文中的GeneratorExit语句引发a yield。未处理的异常将导致清除代码不运行。未处理的异常由上下文管理器引发,并传递回contextmanager装饰器的contextmanager。由于抛出的错误与抛出的相同,__exit__返回False以指示发送给__exit__的原始错误未处理。

最后,这将继续GeneratorExitcoro内部的with块之外的传播,在那里它仍然未被处理。然而,对于生成器来说,不处理GeneratorExits是正常的,所以原始的close方法会抑制GeneratorExit

请参阅yield documentation的这一部分

如果生成器在最后确定之前未恢复(通过达到零引用计数或被垃圾收集),则将调用生成器-迭代器的close()方法,允许执行任何挂起的

子句。

看看我们看到的close documentation

在生成器函数暂停时引发GeneratorExit。如果生成器函数然后优雅地退出,已经关闭,或引发GeneratorExit (通过不捕获异常),则关闭返回给调用方。

with statement documentation的这一部分

  1. 套件被执行。

  1. 调用上下文管理器的exit()方法。如果异常导致套件退出,则将其类型、值和回溯作为参数传递给exit()。否则,将提供三个无参数。

以及用于__exit__ method装饰器的contextmanager代码。

因此,对于所有这些上下文(rim-shot),我们能够获得期望行为的最简单的方法是尝试-除了-最后在上下文管理器的定义中。这是来自contextlib docs的建议方法。他们所有的例子都是这样的。

因此,您可以使用

…除…外语句来捕获错误(如果有的话),或者确保进行某种清理。

代码语言:javascript
复制
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("---!")

现在的产出是:

代码语言:javascript
复制
STARTED
HI
---
1 2
---!
ENDED

如你所愿。

我们还可以用传统的方式定义上下文管理器:作为一个具有__enter____exit__方法的类,并且仍然得到正确的行为:

代码语言:javascript
复制
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__的返回值并不重要)。

票数 1
EN

Stack Overflow用户

发布于 2021-08-19 16:08:07

您可以通过告诉协同线关闭,通过发送一些会导致它脱离循环并返回的东西来做到这一点,如下所示。这样做将导致在执行此操作时引发StopIteration异常,因此我添加了另一个上下文管理器以允许对其进行抑制。注意,我还添加了一个coroutine装饰器,使它们在第一次调用时自动启动,但该部分严格来说是可选的。

代码语言:javascript
复制
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("---!")

输出:

代码语言:javascript
复制
STARTED
HI
---
1 2
3 5
ENDED
---!
票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/68850560

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档