【问题标题】:How are context managers in unconsumed generators finalized?未使用的生成器中的上下文管理器如何最终确定?
【发布时间】:2019-06-11 22:21:12
【问题描述】:

我不明白未完成的生成器中的上下文管理器是如何以及何时关闭的。考虑以下上下文管理器和函数:

from contextlib import contextmanager

@contextmanager
def ctx():
    print('enter ctx')
    yield
    print('exit ctx')

def gen_nums(n):
    with ctx():
        yield from range(n)

我的第一个直觉是,如果我调用 gen_nums 但不完全消耗生成器,ctx 将永远不会关闭,这相当令人担忧。例如:

for i, j in zip(range(5), gen_nums(10)):
    print(f'{i}, {j}')

这里exit ctx 没有打印在末尾。正如我所看到的,这意味着如果我在生成器中有一个文件上下文,它将保持打开状态;但是我随后意识到对文件执行相同操作实际上会正确关闭文件。经过一些测试,我发现如果我这样做了:

from contextlib import contextmanager

@contextmanager
def ctx():
    print('enter ctx')
    try:
        yield
    finally:
        print('exit ctx')

现在exit ctx 打印在最后。所以我想在某个时候会触发一些异常,但我不知道是哪个,在哪里或何时(我试图用except BaseException as e 打印异常,但它不起作用)。删除生成器时似乎会发生这种情况,因为如果我这样做:

g = gen_nums(10)
for i, j in zip(range(5), g):
    print(f'{i}, {j}')
del g

那么exit ctx 只发生在del g 之后。不过,我想更好地了解这里发生了什么以及谁触发了什么。

【问题讨论】:

  • 查看标记的线程,它可能不是完全重复的,但似乎与您的查询相关。也就是说,当del g 被执行时,我怀疑__del__ 有点等价于g.close(),后者在没有StopIteration 的情况下终止生成器以允许释放上下文管理器中的资源。因此,由于生成器没有抛出错误,ctxtry... except 中没有捕获任何内容。但是,如果您在其生命周期内执行了 g.throw(SomeError),您看到 ctx 捕获该错误。
  • 但是我无法完全复制模拟生成器类来暴露内部工作以确认我的怀疑,所以我无法完全提供答案。我在这里理解的一般流程是创建g,触发ctx.__enter__(),在运行生成器时g.send() 等,然后发生g.close() 或类似的情况,并返回ctx.__exit__()(它接受任何错误被抛出)。
  • @Idlehands 谢谢你的链接。它并不能完全解决问题,但它有解决问题的指针。发生的情况是,正如您所建议的,删除未完成的生成器将调用其 stop 方法,这将引发 GeneratorExit 异常,该异常将被引发到上下文管理器。事实上,如果我将ctx 的第二个版本中的finally: 替换为except GeneratorExit:,它也可以工作(我曾尝试在未成功之前打印引发的异常,因为打印GeneratorExit 没有显示任何内容)。
  • 奇怪的是我什至不能做c.__enter__(),它在为我抱怨TypeError: '_GeneratorContextManager' object is not an iterator。我想弄清楚ctxg 中的哪一个正在提高GeneratorExit yb 并添加一些描述,但我只是不够精通,无法深入了解。可能必须使用__enter__ __exit__ 方法构建我自己的Ctx。我不愿提供一个我自己并不完全理解的半开玩笑的答案,所以这就是为什么我把它作为评论留下来希望其他人澄清你的解决方案。

标签: python generator contextmanager


【解决方案1】:

考虑一下:

@contextmanager
def ctx():
    print('enter ctx')
    try:
        print('ctx begins yield')
        yield
        print('ctx finishes yield')
    finally:    
        print('exit ctx')

def gen_nums(n):
    print('entering generator')
    l = range(n)
    with ctx():
        print('inside context manager')
        for i in l:
            print('gen before yield')
            yield i
            print('gen after yield')
        print('existing ctx')
    print('exiting generator')

结果是这样的:

>>> g = gen_nums(3)
>>> next(g)
entering generator
enter ctx
ctx begins yield
inside context manager
gen before yield
0
>>> next(g)
gen after yield
gen before yield
1
>>> next(g)
gen after yield
gen before yield
2
>>> next(g)
gen after yield
exiting ctx
ctx finishes yield
exit ctx
exiting generator
Traceback (most recent call last):
  File "<pyshell#165>", line 1, in <module>
    next(g)
StopIteration

似乎contextmanager 仅在generator 的最后一次迭代之后 并且在它到达StopIteration 之前才真正退出。所以在generator 的最后一次迭代中,如果contextmanager 没有在for 循环中运行(它处理StopIteration),它将保持打开状态。

您将无法在contextmanager 中捕获StopIteration,因为它发生在generator 用尽之后,此时contextmanager 将被退出已经。

另外,IIRC,在构建contextmanager 时,您应该始终使用try... finally...

【讨论】:

  • 如果生成器到达StopIteration,我知道上下文正常退出。我的问题是关于这种情况不会发生的情况。例如,g = gen_nums(3); next(g); del g 仍会打印exit ctx,我想知道它是如何工作的。 finally 确实经常在上下文管理器中使用,尽管我使用它以防with 块内出现异常;我要指出的是,生成器也很重要,即使没有引发异常(至少是明确的)。
猜你喜欢
  • 2018-12-23
  • 2016-07-09
  • 2021-09-19
  • 2023-01-14
  • 2020-10-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-12-25
相关资源
最近更新 更多