14

我最近写了一个返回一系列打开文件的方法;换句话说,是这样的:

# this is very much simplified, of course
# the actual code returns file-like objects, not necessarily files
def _iterdir(self, *path):
    dr = os.path.join(*path)
    paths = imap(lambda fn: os.path.join(dr, fn), os.listdir(dr))

    return imap(open, paths)

从语法上讲,如果我执行以下操作,我希望必须关闭生成的对象:

for f in _iterdir('/', 'usr'):
    make_unicorns_from(f)
    # ! f.close()

因此,我决定包装_iterdir一个上下文管理器:

def iterdir(self, *path):
    it = self._iterdir(*path)

    while 1:
        with it.next() as f:
            yield f

这似乎工作正常。

我感兴趣的是这样做是否是好的做法。按照这种模式我会遇到任何问题(也许如果抛出异常)?

4

2 回答 2

8

我看到有两个问题。一是如果您尝试一次使用多个文件,事情就会中断:

list(iterdir('/', 'usr')) # Doesn't work; they're all closed.

第二个不太可能在 CPython 中发生,但是如果您有一个引用循环,或者您的代码曾经在不同的 Python 实现上运行,那么问题就会显现出来。

如果发生异常make_unicorns_from(f)

for f in iterdir('/', 'usr'):
    make_unicorns_from(f) # Uh oh, not enough biomass.

在生成器被垃圾收集之前,您正在使用的文件不会被关闭。此时close会调用生成器的方法,GeneratorExit在 last 处抛出异常,yield异常会导致上下文管理器关闭文件。

使用 CPython 的引用计数,这通常会立即发生。但是,在非引用计数的实现中或存在引用循环的情况下,可能不会收集生成器,直到运行循环检测 GC pass。这可能需要一段时间。


我的直觉说要关闭文件给调用者。你可以做

for f in _iterdir('/', 'usr'):
    with f:
        make_unicorns_from(f)

with即使在生成器中没有 a ,并且即使抛出异常,它们也会立即关闭。我不知道这实际上是否比让生成器负责关闭文件更好。

于 2014-07-11T07:37:53.370 回答
8

整个要点with涉及将打开和关闭与异常安全和显式生命周期统一起来。您的抽象删除了其中的一些,但不是全部。

这是一个完全简化的示例:

def with_open():
    with open(...) as f:
        yield f

考虑其用法中的一个例外:

for _ in with_open():
    raise NotImplementedError

这不会终止循环,因此文件将保持打开状态。可能永远。

也考虑不完整的、基于非异常的退出:

for _ in with_open():
    break

for _ in with_open():
    return

next(with_open())

一种选择是返回上下文管理器本身,以便您可以执行以下操作:

def with_open():
    yield partial(open, ...)

for filecontext in with_open():
    with filecontext() as f:
        break

另一个更直接的解决方案是将函数定义为

from contextlib import closing

def with_open(self, *path):
    def inner():
        for file in self._iterdir(*path):
            with file:
                yield file

    return closing(inner())

并将其用作

with iterdir() as files:
    for file in files:
        ...

这保证了关闭,而不必将文件的打开移动到调用者。

于 2014-07-11T15:19:47.227 回答