【问题标题】:Is it safe to combine 'with' and 'yield' in python?在python中结合'with'和'yield'是否安全?
【发布时间】:2017-06-12 09:53:56
【问题描述】:

使用上下文管理器自动关闭文件是python中的一个常见习语:

with open('filename') as my_file:
    # do something with my_file

# my_file gets automatically closed after exiting 'with' block

现在我想读取几个文件的内容。数据的消费者不知道也不关心数据是来自文件还是非文件。它不想检查它收到的对象是否可以打开。它只是想从中读取一些内容。所以我创建了一个这样的迭代器:

def select_files():
    """Yields carefully selected and ready-to-read-from files"""
    file_names = [.......]
    for fname in file_names:
        with open(fname) as my_open_file:
            yield my_open_file

这个迭代器可以这样使用:

for file_obj in select_files():
    for line in file_obj:
        # do something useful

(请注意,相同的代码可用于使用的不是打开的文件,而是字符串列表 - 这很酷!)

问题是:产生打开的文件是否安全?

看起来像“为什么不呢?”。消费者调用迭代器,迭代器打开文件,将其交给消费者。消费者处理文件并返回到下一个迭代器。迭代器代码继续,我们退出 'with' 块,my_open_file 对象被关闭,转到下一个文件,等等。

但是如果消费者永远不会返回到下一个文件的迭代器怎么办? F.e.消费者内部发生异常。或者消费者在其中一个文件中发现了一些非常令人兴奋的东西,并高兴地将结果返回给调用它的人?

在这种情况下迭代器代码永远不会恢复,我们永远不会到达 'with' 块的末尾,my_open_file 对象永远不会关闭!

或者会吗?

【问题讨论】:

  • 迭代器超出范围时将被清理,在您提到的情况下应该这样做。
  • 如果您在消费者中保存对生成器的引用(例如,producer=select_files()),那么您可以使用其.throw 方法告诉它关闭。 docs.python.org/3/reference/expressions.html#generator.throw.
  • @TerryJanReedy 生成器有一个close 方法,它更好地服务于停止生成器而不是在那里抛出随机异常...
  • 不管怎样,如果你简单地产生文件的内容,同样的问题会发生:with open(...) as f: for line in f: yield line。消费者可能不会耗尽生成器,因此文件可能永远不会关闭。这通常是“惰性 I/O”的问题。最好在“eager”代码中打开文件并将它们传递给惰性函数。
  • 虽然这不能直接解决 OP 的问题...处理这种情况的另一种方法是使用 fileinput。另见stackoverflow.com/questions/16095855/…

标签: python yield with-statement


【解决方案1】:

您提出了之前提出的批评1。在这种情况下,清理是不确定的,但是当生成器收集垃圾时,发生在 CPython 上。 您的里程可能因其他 python 实现而异...

这是一个简单的例子:

from __future__ import print_function
import contextlib

@contextlib.contextmanager
def manager():
    """Easiest way to get a custom context manager..."""
    try:
        print('Entered')
        yield
    finally:
        print('Closed')


def gen():
    """Just a generator with a context manager inside.

    When the context is entered, we'll see "Entered" on the console
    and when exited, we'll see "Closed" on the console.
    """
    man = manager()
    with man:
        for i in range(10):
            yield i


# Test what happens when we consume a generator.
list(gen())

def fn():
    g = gen()
    next(g)
    # g.close()

# Test what happens when the generator gets garbage collected inside
# a function
print('Start of Function')
fn()
print('End of Function')

# Test what happens when a generator gets garbage collected outside
# a function.  IIRC, this isn't _guaranteed_ to happen in all cases.
g = gen()
next(g)
# g.close()
print('EOF')

在 CPython 中运行这个脚本,我得到:

$ python ~/sandbox/cm.py
Entered
Closed
Start of Function
Entered
Closed
End of Function
Entered
EOF
Closed

基本上,我们看到的是,对于耗尽的生成器,上下文管理器会在您期望的时候进行清理。对于没有耗尽的生成器,当生成器被垃圾收集器收集时,清理功能就会运行。当生成器超出范围时会发生这种情况(或者,最迟在下一个 gc.collect 周期的 IIRC)。

但是,做一些快速实验(例如,在 pypy 中运行上述代码),我并没有清理所有上下文管理器:

$ pypy --version
Python 2.7.10 (f3ad1e1e1d62, Aug 28 2015, 09:36:42)
[PyPy 2.6.1 with GCC 4.2.1 Compatible Apple LLVM 5.1 (clang-503.0.40)]
$ pypy ~/sandbox/cm.py
Entered
Closed
Start of Function
Entered
End of Function
Entered
EOF

因此,上下文管理器的__exit__ 被所有python 实现调用的断言是不真实的。可能这里的未命中归因于pypy's garbage collection strategy不是引用计数),当pypy 决定获取生成器时,进程已经关闭,因此它不会不用理会它...在大多数现实世界的应用程序中,生成器可能会足够快地获得并最终确定,这实际上并不重要...


提供严格的保证

如果您想保证您的上下文管理器已正确完成,您应该在完成时注意close 生成器2。取消注释上面的g.close() 行给了我确定性的清理,因为GeneratorExityield 语句(在上下文管理器中)引发,然后它被生成器捕获/抑制......

$ pypy ~/sandbox/cm.py
Entered
Closed
Start of Function
Entered
Closed
End of Function
Entered
Closed
EOF

$ python3 ~/sandbox/cm.py
Entered
Closed
Start of Function
Entered
Closed
End of Function
Entered
Closed
EOF

$ python ~/sandbox/cm.py
Entered
Closed
Start of Function
Entered
Closed
End of Function
Entered
Closed
EOF

FWIW,这意味着您可以使用 contextlib.closing 清理您的生成器:

from contextlib import closing
with closing(gen_function()) as items:
    for item in items:
        pass # Do something useful!

1最近,一些讨论围绕PEP 533 展开,旨在使迭代器清理更具确定性。
2 sup>关闭一个已经关闭和/或消耗的生成器是完全可以的,这样你就可以调用它而不必担心生成器的状态。

【讨论】:

  • “这种情况下的清理是不确定的”——我不确定我是否完全理解这个说法。这是否意味着发生的事情取决于垃圾收集器的行为?
  • @lesnik -- 是的,就是这个意思。
  • @lesnik -- 我今晚想的更多(也许是因为我并不总是在 my 代码中清理这些东西让我很困扰......) .无论如何,似乎有 一种方法可以强制生成器在您完成它们后进行清理。我已经重写/更新了答案以解释这是怎么可能的。
  • 特别感谢您关注 PEP-533 - 垃圾收集器参与其中对我来说是一个很大的惊喜!
  • 您提到如果迭代器用尽“上下文管理器会在您期望的时候清理(或多或少)”。为什么“或多或少”?在这种情况下情况不是很简单吗?
【解决方案2】:

在 python 中结合 'with' 和 'yield' 是否安全?

我认为你不应该这样做。

让我演示制作一些文件:

>>> for f in 'abc':
...     with open(f, 'w') as _: pass

说服自己文件在那里:

>>> for f in 'abc': 
...     with open(f) as _: pass 

这是一个重新创建代码的函数:

def gen_abc():
    for f in 'abc':
        with open(f) as file:
            yield file

这里看起来可以使用函数了:

>>> [f.closed for f in gen_abc()]
[False, False, False]

但是让我们首先创建一个所有文件对象的列表理解:

>>> l = [f for f in gen_abc()]
>>> l
[<_io.TextIOWrapper name='a' mode='r' encoding='cp1252'>, <_io.TextIOWrapper name='b' mode='r' encoding='cp1252'>, <_io.TextIOWrapper name='c' mode='r' encoding='cp1252'>]

现在我们看到它们都关闭了:

>>> c = [f.closed for f in l]
>>> c
[True, True, True]

这仅在生成器关闭之前有效。然后文件全部关闭。

我怀疑这就是你想要的,即使你使用惰性评估,你的最后一个文件可能会在你完成使用之前关闭。

【讨论】:

  • 您好,请问您认为在 2020 年解决此问题的建议是什么?
猜你喜欢
  • 2020-03-29
  • 1970-01-01
  • 2012-10-15
  • 1970-01-01
  • 1970-01-01
  • 2020-02-22
  • 2010-10-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多