一次打开多个(未指定数量)文件并确保它们正确关闭

时间:2011-10-30 13:28:56

标签: python exception python-3.x with-statement

我知道我可以打开多个文件,比如

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是否已正确定义,并且文件已设置为打开?

4 个答案:

答案 0 :(得分:13)

不,除非所有fs次调用成功完成,否则您的代码不会初始化open()。这应该工作:

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

    ....

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

另请注意,f.close()可能会失败,因此您可能希望捕获并忽略(或以其他方式处理)任何失败。

答案 1 :(得分:7)

当然,为什么不呢,这是一个应该做的配方。创建一个上下文管理器“池”,可以输入任意数量的上下文(通过调用它的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),这仍然会允许引发异常。

答案 2 :(得分:1)

尝试打开文件时,尝试从文件读取时,以及(极少)尝试关闭文件时,可能会发生错误。

因此基本的错误处理结构可能如下所示:

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块)。

您是否真的需要并行打开文件,还是可以按顺序处理?如果是后者,则应将类似上述文件处理代码的内容放入函数中,然后为列表中的每个路径调用该函数。

答案 3 :(得分:1)

感谢您的所有答案。从你们所有人那里获得灵感,我想出了以下几点。我认为(希望)它按照我的意图行事。我不确定是否将其作为答案或问题的补充发布,但认为答案更合适,如果它没有按照我的要求做适当的评论。

它可以像这样使用..

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