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

时间:2014-07-11 06:18:38

标签: python generator contextmanager

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

# 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

这似乎工作正常。

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

2 个答案:

答案 0 :(得分: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点处抛出yield异常,异常将导致上下文管理器关闭文件。

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


我的直觉是将文件关闭给来电者。你可以做到

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

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

答案 1 :(得分:5)

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

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