问题 从上下文管理器中收益是一种好的做法吗?


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

# 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

这似乎工作正常。

我感兴趣的是这样做是否是好的做法。我是否会遇到这种模式之后的任何问题(可能会抛出异常)?


11173
2017-07-11 06:18


起源

看起来很好,我使用自定义上下文管理器进行了快速检查 __exit__ 即使发生异常,也被称为罚款。 - Ashwini Chaudhary


答案:


我看到有两个问题。一个是如果你一次尝试使用多个文件,事情会破坏:

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 最后一点的例外 yield,该异常将导致上下文管理器关闭该文件。

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


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

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

他们都会被迅速关闭,即使没有 with 在生成器中,即使抛出异常。我不知道这是否比生成器负责关闭文件更好。


7
2017-07-11 07:37



你提到的第一个问题看起来像你忘记原因那样难以调试的东西!我认为仅仅因为这个原因,这种模式是一个坏主意。 - sapi
此外,有点切线,谢谢你明确表示你可以安全地筑巢 with 语句(如,将上下文管理应用于已打开的文件)。我不知道那是可能的。 - sapi


整点 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:
        ...

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


5
2017-07-11 15:19



感谢您的反馈。我不知道破坏或返回不会清理经理;这是一个严重的问题。 - sapi