我最近写了一个返回一系列打开文件的方法;换句话说,这样的事情:
# 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
这似乎工作正常。
我感兴趣的是这样做是否是好的做法。我是否会遇到这种模式之后的任何问题(可能会抛出异常)?
我看到有两个问题。一个是如果你一次尝试使用多个文件,事情会破坏:
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
在生成器中,即使抛出异常。我不知道这是否比生成器负责关闭文件更好。
整点 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:
...
这保证了关闭,而无需将文件的打开移动到调用者。