Понимание времени жизни и закрытия генераторов
В этом шаге мы рассмотрим время жизни генераторов Python и научимся правильно их закрывать. Генераторы в Python - это особый тип итераторов, которые позволяют генерировать последовательность значений по мере необходимости, а не вычислять их все сразу и хранить в памяти. Это может быть очень полезно при работе с большими наборами данных или бесконечными последовательностями.
Что такое генератор follow()
?
Начнем с рассмотрения файла follow.py
в директории проекта. Этот файл содержит функцию - генератор с именем follow()
. Функция - генератор определяется как обычная функция, но вместо ключевого слова return
она использует yield
. Когда вызывается функция - генератор, она возвращает объект - генератор, по которому можно итерироваться, чтобы получить значения, которые он выдает.
Функция - генератор follow()
непрерывно считывает строки из файла и выдает каждую строку по мере ее считывания. Это похоже на команду Unix tail -f
, которая непрерывно отслеживает файл на предмет новых строк.
Откройте файл follow.py
в редакторе WebIDE:
import os
import time
def follow(filename):
with open(filename,'r') as f:
f.seek(0,os.SEEK_END)
while True:
line = f.readline()
if line == '':
time.sleep(0.1) ## Sleep briefly to avoid busy wait
continue
yield line
В этом коде оператор with open(filename, 'r') as f
открывает файл в режиме чтения и гарантирует, что он будет правильно закрыт при выходе из блока. Строка f.seek(0, os.SEEK_END)
перемещает указатель файла в конец файла, чтобы генератор начал чтение с конца. Цикл while True
непрерывно считывает строки из файла. Если строка пустая, это означает, что пока нет новых строк, поэтому программа засыпает на 0,1 секунды, чтобы избежать активного ожидания, и затем переходит к следующей итерации. Если строка не пустая, она выдается.
Этот генератор работает в бесконечном цикле, что вызывает важный вопрос: что происходит, когда мы перестаем использовать генератор или хотим остановить его раньше?
Модификация генератора для обработки закрытия
Мы должны модифицировать функцию follow()
в файле follow.py
, чтобы обработать случай, когда генератор корректно закрывается. Для этого мы добавим блок try - except
, который перехватывает исключение GeneratorExit
. Исключение GeneratorExit
возникает, когда генератор закрывается, либо в результате сборки мусора, либо при вызове метода close()
.
import os
import time
def follow(filename):
try:
with open(filename,'r') as f:
f.seek(0,os.SEEK_END)
while True:
line = f.readline()
if line == '':
time.sleep(0.1) ## Sleep briefly to avoid busy wait
continue
yield line
except GeneratorExit:
print('Following Done')
В этом модифицированном коде блок try
содержит основную логику генератора. Если возникает исключение GeneratorExit
, блок except
его перехватывает и выводит сообщение 'Following Done'. Это простой способ выполнить действия по очистке, когда генератор закрывается.
Сохраните файл после внесения этих изменений.
Эксперименты с закрытием генераторов
Теперь проведем несколько экспериментов, чтобы увидеть, как ведут себя генераторы при сборке мусора или явном закрытии.
Откройте терминал и запустите интерпретатор Python:
cd ~/project
python3
Эксперимент 1: Сборка мусора работающего генератора
>>> from follow import follow
>>> ## Experiment: Garbage collection of a running generator
>>> f = follow('stocklog.csv')
>>> next(f)
'"MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314\n'
>>> del f ## Delete the generator object
Following Done ## This message appears because of our GeneratorExit handler
В этом эксперименте мы сначала импортируем функцию follow
из файла follow.py
. Затем создаем объект - генератор f
, вызвав follow('stocklog.csv')
. Мы используем функцию next()
для получения следующей строки из генератора. Наконец, удаляем объект - генератор с помощью оператора del
. Когда объект - генератор удаляется, он автоматически закрывается, что вызывает наш обработчик исключения GeneratorExit
, и выводится сообщение 'Following Done'.
Эксперимент 2: Явное закрытие генератора
>>> f = follow('stocklog.csv')
>>> for line in f:
... print(line, end='')
... if 'IBM' in line:
... f.close() ## Explicitly close the generator
...
"MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
"VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
"HPQ",45.76,"6/11/2007","09:34.29",0.06,45.80,45.76,45.59,257169
"GM",31.45,"6/11/2007","09:34.31",0.45,31.00,31.50,31.45,582429
"IBM",102.86,"6/11/2007","09:34.44",-0.21,102.87,102.86,102.77,147550
Following Done
>>> for line in f:
... print(line, end='') ## No output: generator is closed
...
В этом эксперименте мы создаем новый объект - генератор f
и итерируемся по нему с помощью цикла for
. Внутри цикла мы выводим каждую строку и проверяем, содержит ли строка строку 'IBM'. Если содержит, мы вызываем метод close()
у генератора, чтобы явно закрыть его. Когда генератор закрывается, возникает исключение GeneratorExit
, и наш обработчик исключений выводит сообщение 'Following Done'. После закрытия генератора, если мы попытаемся итерироваться по нему снова, не будет вывода, так как генератор больше не активен.
Эксперимент 3: Выход из итерации и продолжение работы генератора
>>> f = follow('stocklog.csv')
>>> for line in f:
... print(line, end='')
... if 'IBM' in line:
... break ## Break out of the loop, but don't close the generator
...
"MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
"VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
"HPQ",45.76,"6/11/2007","09:34.29",0.06,45.80,45.76,45.59,257169
"GM",31.45,"6/11/2007","09:34.31",0.45,31.00,31.50,31.45,582429
"IBM",102.86,"6/11/2007","09:34.44",-0.21,102.87,102.86,102.77,147550
>>> ## Resume iteration - the generator is still active
>>> for line in f:
... print(line, end='')
... if 'IBM' in line:
... break
...
"CAT",78.36,"6/11/2007","09:37.19",-0.16,78.32,78.36,77.99,237714
"VZ",42.99,"6/11/2007","09:37.20",-0.08,42.95,42.99,42.78,268459
"IBM",102.91,"6/11/2007","09:37.31",-0.16,102.87,102.91,102.77,190859
>>> del f ## Clean up
Following Done
В этом эксперименте мы создаем объект - генератор f
и итерируемся по нему с помощью цикла for
. Внутри цикла мы выводим каждую строку и проверяем, содержит ли строка строку 'IBM'. Если содержит, мы используем оператор break
для выхода из цикла. Выход из цикла не закрывает генератор, поэтому генератор все еще активен. Затем мы можем продолжить итерацию, начав новый цикл for
по тому же объекту - генератору. Наконец, мы удаляем объект - генератор для очистки, что вызывает обработчик исключения GeneratorExit
.
Основные выводы
- Когда генератор закрывается (либо в результате сборки мусора, либо при вызове
close()
), внутри генератора возникает исключение GeneratorExit
.
- Вы можете перехватить это исключение, чтобы выполнить действия по очистке при закрытии генератора.
- Выход из итерации по генератору (с помощью
break
) не закрывает генератор, что позволяет продолжить его работу позже.
Выйдите из интерпретатора Python, введя exit()
или нажав Ctrl + D
.