Как выйти из генератора Python с открытыми файловыми дескрипторами - PullRequest
4 голосов
/ 03 сентября 2010

Я пишу генератор Python, который выглядит как "кот". Мой конкретный случай использования для "grep-подобной" операции. Я хочу, чтобы он мог выйти из генератора, если выполнено условие:

summary={}
for fn in cat("filelist.dat"):
    for line in cat(fn):
        if line.startswith("FOO"):
            summary[fn] = line
            break

Поэтому, когда происходит break, мне нужен генератор cat() для завершения и закрытия дескриптора файла для fn.

Мне нужно прочитать 100 тыс. Файлов с 30 ГБ общих данных, и ключевое слово FOO встречается в области заголовка, поэтому в этом случае важно, чтобы функция cat() перестала читать файл как можно скорее.

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

Спасибо

Ian

Ответы [ 4 ]

5 голосов
/ 03 сентября 2010

Используя протокол контекста и протокол итератора в одном объекте, вы можете написать довольно приятный код, подобный этому:

with cat("/etc/passwd") as lines:
    for line in lines:
        if "mail" in line:
            print line.strip()
            break

Это пример реализации, протестированный с Python 2.5 на Linux. Он читает строки /etc/passwd, пока не найдет строку для пользователя audio, а затем останавливается:

from __future__ import with_statement


class cat(object):

    def __init__(self, fname):
        self.fname = fname

    def __enter__(self):
        print "[Opening file %s]" % (self.fname,)
        self.file_obj = open(self.fname, "rt")
        return self

    def __exit__(self, *exc_info):
        print "[Closing file %s]" % (self.fname,)
        self.file_obj.close()

    def __iter__(self):
        return self

    def next(self):
        line = self.file_obj.next().strip()
        print "[Read: %s]" % (line,)
        return line


def main():
    with cat("/etc/passwd") as lines:
        for line in lines:
            if "mail" in line:
                print line.strip()
                break


if __name__ == "__main__":
    import sys
    sys.exit(main())

Или даже проще:

with open("/etc/passwd", "rt") as f:
    for line in f:
        if "mail" in line:
            break

Файловые объекты реализуют протокол итератора (см. http://docs.python.org/library/stdtypes.html#file-objects)

5 голосов
/ 03 сентября 2010

Генераторы имеют метод close, который вызывает GeneratorExit в операторе yield. Если вы специально перехватили это исключение, вы можете запустить какой-нибудь код разрыва:

import contextlib
with contextlib.closing( cat( fn ) ):
    ...

, а затем в cat:

try:
    ...
except GeneratorExit:
    # close the file

Если вам нужен более простой способ сделать это (без использования тайного метода close в генераторах), просто заставьте cat взять файлоподобный объект вместо строки для открытия и обработать файл IO себя:

for filename in filenames:
    with open( filename ) as theFile:
        for line in cat( theFile ):
            ...

Однако вам не нужно беспокоиться об этом, потому что сборщик мусора справится со всем этим. Тем не менее,

явное лучше, чем неявное

1 голос
/ 27 июля 2015

Пожалуйста, рассмотрите также этот пример:

def itertest():
    try:
        for i in xrange(1000):
            print i
            yield i
    finally:
        print 'finally'

x = itertest()

for i in x:
    if i > 2:
        break

print 'del x'
del x

print 'exit'

0
1
2
3
del x
finally
exit

Это показывает, что, наконец, запускается после очистки итератора. Я думаю, что __del__(self) звонит self.close(), см. Также здесь: https://docs.python.org/2.7/reference/expressions.html#generator.close

0 голосов
/ 17 июля 2015

Кажется, есть еще одна возможность использовать try..finally (протестировано на Python 2.7.6):

def gen():
    i = 0
    try:
        while True:
            print 'yield %i' % i
            yield i
            i += 1
        print 'will never get here'
    finally:
        print 'done'

for i in gen():
    if i > 1:
        print 'break'
        break
    print i

Дает мне следующую распечатку:

yield 0
0
yield 1
1
yield 2
break
done
...