【发布时间】: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 的情况下终止生成器以允许释放上下文管理器中的资源。因此,由于生成器没有抛出错误,ctx在try... 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。我想弄清楚ctx或g中的哪一个正在提高GeneratorExityb 并添加一些描述,但我只是不够精通,无法深入了解。可能必须使用__enter____exit__方法构建我自己的Ctx。我不愿提供一个我自己并不完全理解的半开玩笑的答案,所以这就是为什么我把它作为评论留下来希望其他人澄清你的解决方案。
标签: python generator contextmanager