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

Безопасно ли комбинировать 'with' и 'yield' в python?

Это обычная идиома в python для использования диспетчера контекстов для автоматического закрытия файлов:

with open('filename') as my_file:
    # do something with my_file

# my_file gets automatically closed after exiting 'with' block

Теперь я хочу прочитать содержимое нескольких файлов. Потребитель данных не знает и не заботится о том, поступают ли данные из файлов или нет файлов. Он не хочет проверять, могут ли объекты, которые он получил, открыть или нет. Он просто хочет получить что-то, чтобы читать строки. Поэтому я создаю итератор следующим образом:

def select_files():
    """Yields carefully selected and ready-to-read-from files"""
    file_names = [.......]
    for fname in file_names:
        with open(fname) as my_open_file:
            yield my_open_file

Этот итератор может использоваться следующим образом:

for file_obj in select_files():
    for line in file_obj:
        # do something useful

(Обратите внимание, что один и тот же код можно использовать, чтобы потреблять не открытые файлы, а списки строк - это круто!)

Вопрос: безопасно ли открывать открытые файлы?

Похоже, почему бы и нет? Потребитель вызывает итератор, итератор открывает файл, возвращает его потребителю. Потребитель обрабатывает файл и возвращается к итератору для следующего. Итерационный код возобновляется, мы выходим из блока "с", объект my_open_file закрывается, переходит к следующему файлу и т.д.

Но что, если потребитель никогда не возвращается к итератору для следующего файла? F.E. исключение произошло внутри потребителя. Или потребитель нашел что-то очень интересное в одном из файлов и счастливо вернул результаты тому, кто его назвал?

В этом случае код Итератора никогда не возобновится, мы никогда не дойдем до конца с блоком, а объект my_open_file никогда не будет закрыт!

Или это?

4b9b3361

Ответ 1

Вы поднимаете критику, которая была поднята до 1. Очистка в этом случае недетерминирована, но это произойдет с CPython, когда генератор получает собранный мусор. Ваш пробег может отличаться для других реализаций python...

Вот пример:

from __future__ import print_function
import contextlib

@contextlib.contextmanager
def manager():
    """Easiest way to get a custom context manager..."""
    try:
        print('Entered')
        yield
    finally:
        print('Closed')


def gen():
    """Just a generator with a context manager inside.

    When the context is entered, we'll see "Entered" on the console
    and when exited, we'll see "Closed" on the console.
    """
    man = manager()
    with man:
        for i in range(10):
            yield i


# Test what happens when we consume a generator.
list(gen())

def fn():
    g = gen()
    next(g)
    # g.close()

# Test what happens when the generator gets garbage collected inside
# a function
print('Start of Function')
fn()
print('End of Function')

# Test what happens when a generator gets garbage collected outside
# a function.  IIRC, this isn't _guaranteed_ to happen in all cases.
g = gen()
next(g)
# g.close()
print('EOF')

Запустив этот script в CPython, я получаю:

$ python ~/sandbox/cm.py
Entered
Closed
Start of Function
Entered
Closed
End of Function
Entered
EOF
Closed

В основном, мы видим, что для истощенных генераторов менеджер контекста очищает, когда вы ожидаете. Для генераторов, которые не исчерпаны, функция очистки работает, когда генератор собирается сборщиком мусора. Это происходит, когда генератор выходит из области действия (или, IIRC в следующий gc.collect цикл не позднее).

Однако, выполняя некоторые быстрые эксперименты (например, запуская приведенный выше код в pypy), я не очищаю всех моих менеджеров контекста:

$ pypy --version
Python 2.7.10 (f3ad1e1e1d62, Aug 28 2015, 09:36:42)
[PyPy 2.6.1 with GCC 4.2.1 Compatible Apple LLVM 5.1 (clang-503.0.40)]
$ pypy ~/sandbox/cm.py
Entered
Closed
Start of Function
Entered
End of Function
Entered
EOF

Итак, утверждение, что диспетчер контекста __exit__ будет вызван для всех реализаций python, неверен. Вероятно, промахи здесь связаны с стратегией сбора мусора pypy (которая не является подсчетом ссылок), и к моменту времени pypy решает собрать генераторы, процесс уже закрывается, и поэтому он не беспокоится об этом... В большинстве приложений реального мира генераторы, вероятно, получат прибыль и завершатся достаточно быстро, чтобы на самом деле это не имело значения...


Предоставление строгих гарантий

Если вы хотите, чтобы ваш менеджер контекста был правильно доработан, вы должны позаботиться о close генераторе, когда закончите с это 2. Разоружение строк g.close() выше дает мне детерминированную очистку, потому что GeneratorExit возникает в выражении yield (который находится внутри диспетчера контекста), а затем он улавливается/подавляется генератором...

$ pypy ~/sandbox/cm.py
Entered
Closed
Start of Function
Entered
Closed
End of Function
Entered
Closed
EOF

$ python3 ~/sandbox/cm.py
Entered
Closed
Start of Function
Entered
Closed
End of Function
Entered
Closed
EOF

$ python ~/sandbox/cm.py
Entered
Closed
Start of Function
Entered
Closed
End of Function
Entered
Closed
EOF

FWIW, это означает, что вы можете очистить свои генераторы с помощью contextlib.closing:

from contextlib import closing
with closing(gen_function()) as items:
    for item in items:
        pass # Do something useful!

1 Совсем недавно некоторые обсуждения вращались вокруг PEP 533, целью которого является очистка итератора больше детерминированным.
2 Вполне нормально закрыть уже закрытый и/или потребляемый генератор, чтобы вы могли его вызвать, не беспокоясь о состоянии генератора.

Ответ 2

Безопасно ли комбинировать 'with' и 'yield' в python?

Я не думаю, что вы должны это сделать.

Позвольте мне продемонстрировать некоторые файлы:

>>> for f in 'abc':
...     with open(f, 'w') as _: pass

Убедите себя, что файлы есть:

>>> for f in 'abc': 
...     with open(f) as _: pass 

И вот функция, которая воссоздает ваш код:

def gen_abc():
    for f in 'abc':
        with open(f) as file:
            yield file

Здесь вы можете использовать функцию:

>>> [f.closed for f in gen_abc()]
[False, False, False]

Но сначала создайте представление списка всех объектов файла:

>>> l = [f for f in gen_abc()]
>>> l
[<_io.TextIOWrapper name='a' mode='r' encoding='cp1252'>, <_io.TextIOWrapper name='b' mode='r' encoding='cp1252'>, <_io.TextIOWrapper name='c' mode='r' encoding='cp1252'>]

И теперь мы видим, что все они закрыты:

>>> c = [f.closed for f in l]
>>> c
[True, True, True]

Это работает только до тех пор, пока генератор не закроется. Затем файлы закрыты.

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