Открытие сразу нескольких (неопределенного количества) файлов и обеспечение их правильного закрытия - PullRequest
20 голосов
/ 30 октября 2011

Я знаю, что могу открыть несколько файлов с чем-то вроде

with open('a', 'rb') as a, open('b', 'rb') as b:

Но у меня есть ситуация, когда у меня есть список файлов для открытия, и мне интересно, какой предпочтительный метод выполнениято же самое, когда количество файлов заранее неизвестно.Что-то вроде

with [ open(f, 'rb') for f in files ] as fs:

(но это не так с AttributeError, поскольку список не реализует __exit__)

Я не против использовать что-то вроде

try:
    fs = [ open(f, 'rb') for f in files ]

    ....

finally:
    for f in fs:
        f.close()

Но я не уверен, что произойдет, если некоторые файлы будут выброшены при попытке открыть их.Будет ли fs правильно определен с файлами, которые удалось открыть, в блоке finally?

Ответы [ 5 ]

13 голосов
/ 30 октября 2011

Нет, ваш код не будет инициализироваться fs, если все open() вызовы не будут завершены успешно. Это должно работать, хотя:

fs = []
try:
    for f in files:
        fs.append(open(f, 'rb'))

    ....

finally:
    for f in fs:
        f.close()

Также обратите внимание, что f.close () может завершиться с ошибкой, поэтому вы можете захотеть перехватить и проигнорировать (или иным образом обработать) любые сбои.

7 голосов
/ 30 октября 2011

Конечно, почему бы и нет, вот рецепт, который должен это сделать.Создайте диспетчер контекста «пул», который может вводить произвольное количество контекстов (вызывая его метод enter()), и они будут очищены в конце конца набора.

class ContextPool(object):
    def __init__(self):
        self._pool = []

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, exc_tb):
        for close in reversed(self._pool):
            close(exc_type, exc_value, exc_tb)

    def enter(self, context):
        close = context.__exit__
        result = context.__enter__()
        self._pool.append(close)
        return result

Дляпример:

>>> class StubContextManager(object):
...     def __init__(self, name):
...         self.__name = name
...     def __repr__(self):
...         return "%s(%r)" % (type(self).__name__, self.__name)
... 
...     def __enter__(self):
...          print "called %r.__enter__()" % (self)
... 
...     def __exit__(self, *args):
...          print "called %r.__exit__%r" % (self, args)
... 
>>> with ContextPool() as pool:
...     pool.enter(StubContextManager("foo"))
...     pool.enter(StubContextManager("bar"))
...     1/0
... 
called StubContextManager('foo').__enter__()
called StubContextManager('bar').__enter__()
called StubContextManager('bar').__exit__(<type 'exceptions.ZeroDivisionError'>, ZeroDivisionError('integer division or modulo by zero',), <traceback object at 0x02958648>)
called StubContextManager('foo').__exit__(<type 'exceptions.ZeroDivisionError'>, ZeroDivisionError('integer division or modulo by zero',), <traceback object at 0x02958648>)

Traceback (most recent call last):
  File "<pyshell#67>", line 4, in <module>
    1/0
ZeroDivisionError: integer division or modulo by zero
>>> 

Предостережения: менеджеры контекста не должны вызывать исключения в своих __exit__() методах, но если они это делают, этот рецепт не выполняет очистку для всех менеджеров контекста.Точно так же, даже если каждый менеджер контекста указывает, что исключение следует игнорировать (возвращая True из их методов выхода), это все равно позволит вызвать исключение.

1 голос
/ 31 октября 2011

Спасибо за все ваши ответы. Черпая вдохновение у всех вас, я пришел к следующему. Я думаю (надеюсь), что это работает, как я хотел. Я не был уверен, стоит ли публиковать его в качестве ответа или дополнения к вопросу, но подумал, что ответ более уместен, так как тогда, если он не выполняет то, что я задал, его можно прокомментировать соответствующим образом.

Может использоваться, например, вот так.

with contextlist( [open, f, 'rb'] for f in files ) as fs:
    ....

или как это ..

f_lock = threading.Lock()
with contextlist( f_lock, ([open, f, 'rb'] for f in files) ) as (lock, *fs):
    ....

И вот оно,

import inspect
import collections
import traceback

class contextlist:

    def __init__(self, *contexts):

        self._args = []

        for ctx in contexts:
            if inspect.isgenerator(ctx):
                self._args += ctx 
            else:
                self._args.append(ctx)


    def __enter__(self):

        if hasattr(self, '_ctx'):
            raise RuntimeError("cannot reenter contextlist")

        s_ctx = self._ctx = []

        try:
            for ctx in self._args:

                if isinstance(ctx, collections.Sequence):
                    ctx = ctx[0](*ctx[1:])

                s_ctx.append(ctx)

                try:
                    ctx.__enter__()
                except Exception:
                    s_ctx.pop()
                    raise

            return s_ctx

        except:
            self.__exit__()
            raise


    def __exit__(self, *exc_info):

        if not hasattr(self, '_ctx'):
            raise RuntimeError("cannot exit from unentered contextlist")

        e = []

        for ctx in reversed(self._ctx):
            try:
                ctx.__exit__()
            except Exception:
                e.append(traceback.format_exc())

        del self._ctx

        if not e == []: 
            raise Exception('\n>   '*2+(''.join(e)).replace('\n','\n>   '))
1 голос
/ 30 октября 2011

Ошибки могут возникать при попытке открыть файл, при попытке чтения из файла и (очень редко) при попытке закрыть файл.

Таким образом, базовая структура обработки ошибок может выглядеть следующим образом:

try:
    stream = open(path)
    try:
        data = stream.read()
    finally:
        stream.close()
except EnvironmentError as exception:
    print 'ERROR:', str(exception)
else:
    print 'SUCCESS'
    # process data

Это гарантирует, что close будет вызываться всегда, если существует переменная stream. Если stream не существует, значит, open должен был завершиться с ошибкой, и поэтому нет файла для закрытия (в этом случае блок исключений будет выполнен немедленно).

Вам действительно нужно, чтобы файлы открывались параллельно или они могут обрабатываться последовательно? Если последнее, то что-то подобное приведенному выше коду обработки файлов должно быть помещено в функцию, которая затем вызывается для каждого пути в списке.

0 голосов
/ 10 мая 2018

Класс ExitStack из модуля contextlib обеспечивает необходимую функциональность.Канонический вариант использования, упомянутый в документации, управляет динамическим числом файлов.

with ExitStack() as stack:
    files = [stack.enter_context(open(fname)) for fname in filenames]
    # All opened files will automatically be closed at the end of
    # the with statement, even if attempts to open files later
    # in the list raise an exception
...