Подтвердить что ты не робот

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

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

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?

4b9b3361

Ответ 1

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

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

    ....

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

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

Ответ 2

Конечно, почему бы и нет, вот рецепт, который должен это сделать. Создайте пул диспетчера контекста, который может ввести произвольное количество контекстов (путем вызова метода 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 из их методов выхода), это все равно позволит повысить исключение.

Ответ 3

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

Итак, основная структура обработки ошибок может выглядеть так:

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 должен быть сбой, и поэтому нет файла для закрытия (в этом случае блок except будет выполняться немедленно).

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

Ответ 4

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

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

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>   '))