如果在“with”块内完成,“yield”会触发 __exit__ 函数吗?

Posted

技术标签:

【中文标题】如果在“with”块内完成,“yield”会触发 __exit__ 函数吗?【英文标题】:Does "yield"-ing trigger the __exit__ function if done inside a "with" block? 【发布时间】:2017-07-04 15:40:29 【问题描述】:

This question as been asked with respect to returning 来自 with 块内部,但是让步怎么样?

如果下次调用该函数,是否会在 yield 上调用块的 __exit__,然后再次调用 __enter__?还是等待生成器退出 with 块(或返回)

举个例子:

def tmp_csv_file():
    tmp_path = 'tmp.csv'
    with open(tmp_path, 'w+') as csv_file:
        yield csv_file # will this close the file?
    os.remove(tmp_path)

【问题讨论】:

我认为不会 - 生成器保存它们产生的状态,以便它们可以从那个点继续。您没有使用内置的tempfile 有什么原因吗? 你不能自己测试一下吗(因为你显然也没有花时间阅读文档)? 注意:“with”块被称为上下文管理器。 :) @TemporalWolf 我不知道它的存在,谢谢你的提示! @martineau 绝对可以并且将会有,但是我在 SO 上发帖也是为了让其他人可以访问答案。此外,MSeifert 的回答发现了一些我不会测试的点,所以在这里询问确实得到了回报:) 【参考方案1】:

这取决于:

如果您的生成器函数超出范围(或以其他方式被删除),则调用 __exit__

如果生成器耗尽,则调用__exit__

=> 只要生成器在 with-block 中并且没有耗尽,并且保存生成器的变量没有被删除,__exit__ 就不会被调用。

例如:

class Test(object):
    def __init__(self):
        print('init')

    def __enter__(self):
        print('enter')

    def __exit__(self, *args, **kwargs):
        print('exit')


def funfunc():
    with Test():
        yield 1
        yield 2

测试它:

>>> a = funfunc()
>>> next(a)  # no exit
init
enter
1
>>> next(a)  # no exit
2
>>> next(a, 0)  # exit because generator leaves the with-context inside the function
exit
0

或者如果手动删除:

>>> a = funfunc()
>>> next(a)
init
enter
1
>>> del a  # exit because the variable holding the suspended generator is deleted.
exit

【讨论】:

接受了这个,因为添加的关于删除和范围的信息使它成为更完整的答案 您不必耗尽发电机,只需将其从with 中推进即可。至于删除它,生成器在垃圾回收时是 closed,但这并不总是立即发生或根本不会发生(尤其是在 CPython 以外的任何东西上),即使在 @987654331 时,有问题的生成器也可能不会退出 with @d. @user2357112 但耗尽是一种可能性(因为它相当于return None) - 即使没有立即退出上下文管理器,它最终也会在对象被销毁时退出(你有证据吗?对于不这样做的实现还是只是猜测?)。但是,您的最后一点很有趣:您所说的不退出 with 的“错误”生成器是什么意思? @MSeifert: If the generator catches the GeneratorExit exception and doesn't exit、finally 块和 __exit__ 方法可能无法运行。【参考方案2】:

让我们根据经验进行测试。

class MyContextManager:
    def __enter__(self):
        pass
    def __exit__(self, *args):
        print "Context manager is exiting."


def f():
    print("Entered Function.")
    with MyContextManager() as m:
        print("Entered with statement. Yielding...")
        yield m
        print("Yielded. About to exit with statement.")
    print("Now outside of with statement.")

for x in f():
    pass

输出:

C:\Users\Kevin\Desktop>test.py
Entered Function.
Entered with statement. Yielding...
Yielded. About to exit with statement.
Context manager is exiting.
Now outside of with statement.

“上下文管理器正在退出”消息出现在“About to exit with statement”消息之后,因此我们可以得出结论,yield 语句不会触发__exit__ 方法。

【讨论】:

【参考方案3】:

yieldwith 内不会触发__exit__。生成器内部的控制流必须离开with 块,以便__exit__ 触发;暂停生成器不算离开with 块。这在本质上类似于上下文切换到另一个线程不会触发__exit__

【讨论】:

【参考方案4】:

简而言之:no,它会从到达yield 语句的那一刻起暂停该方法。如果您要求下一个元素,则执行剩余部分。

假设你写了result = tmp_csv_file() 什么都没做:所以甚至tmp_path='tmp.csv' 都没有被执行。

现在,如果您调用 next(result),Python 将开始评估函数直到它遇到第一个 yield 语句。所以它执行tmp_path = 'tmp.csv'open(..)s 文件和__enter__s 环境。它命中yield 语句并因此返回csv_file。现在你可以用那个文件做任何你想做的事情。该文件将保持打开状态(只要您没有明确地close() 它),并且__exit__不会被调用

如果您第二次调用next(result),Python 将继续寻找下一个yield 语句。因此它将__exit__with 环境,并删除文件(os.remove(tmp_path))。然后它到达方法的结尾。这意味着我们完成了。因此next(..) 会抛出一个可迭代已用完的错误。

【讨论】:

以上是关于如果在“with”块内完成,“yield”会触发 __exit__ 函数吗?的主要内容,如果未能解决你的问题,请参考以下文章

您能否根据 around_action 过滤器中的 yield 块内发生的情况有条件地显示一条 Flash 消息?

yield函数 ,interrupt函数

当元素在敲除“with”绑定中时,jQuery自动完成不会触发动作

ngFor 块内模板中的异步管道触发 http GET 调用循环

python基础-生成器

Python:lambda表达式和yield关键字理解与使用讲解