Является ли хорошей практикой выход из контекста?
Недавно я написал метод, который возвращает последовательность открытых файлов; другими словами, что-то вроде этого:
# 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 ответа
Я вижу две проблемы. Одна из них заключается в том, что если вы попытаетесь использовать более одного файла за раз, все пойдет не так:
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:
...
Это гарантирует закрытие без необходимости переноса открытия файлов для вызывающей стороны.