我最近编写了一个返回一系列打开文件的方法;换句话说,就像这样:
# 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
这似乎工作正常。
我感兴趣的是这样做是否很好。我是否会遇到这种模式之后的任何问题(可能会抛出异常)?
答案 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:
...
这可以保证关闭,而无需将文件的开头移动到调用者。