Python: Декларативное программирование
Теория: Функции-генераторы
У декларативного подхода к описанию последовательностей есть много достоинств.
Но иногда возникает необходимость поступиться декларативностью и применить императивные приемы — например, изменяемое состояние или возможность досрочно прервать процесс генерации. А еще иногда элементы выходной последовательности зависят друг от друга или от элементов входной последовательности не настолько явно, чтобы можно было обойтись декларативными методами.
Проще говоря, иногда нужно спуститься на такой уровень, на котором выдача элементов наружу проста и контролируема — например, как вывод элементов на печать с помощью print(). Такой код будет выглядеть очень императивно, но зато он будет эффективным.
В Python сама концепция итеративных вычислений прослеживается повсеместно, поэтому средства низкоуровневого программирования потоков данных встроены в сам язык. Именно их мы и изучим в этом уроке.
Ключевое слово yield
Представим, что нам нужно построить последовательность чисел, элементы которой возрастают экспоненциально. Если бы такие числа нужно было лишь распечатать, то код бы мог выглядеть так:
Как только мы вызовем процедуру iterate, то все возрастающие числа будут выводиться бесконечно, ведь никакого завершения цикла мы не предусмотрели. Выполнение процедуры iterate никогда не завершится, поэтому и весь код после вызова не выполнится никогда.
А теперь представим, что нам нужна последовательность за пределами процедуры iterate. В этом случае мы не сможем сделать return вместо print() — это приведет к остановке процесса генерации.
В виде аргумента мы могли бы передать список, в который процедура бы добавляла элементы вместо вывода на печать. Однако использовать список мы не сможем, ведь процедура никогда не завершится.
Далеко не всегда можно заранее узнать, сколько итераций нужно выполнить. Чтобы справиться с описанными выше проблемами, понадобится новое ключевое слово yield:
Заметьте, что вызов функции iterate вычислился в объект-генератор <generator object>, сама же функция не зациклилась. Теперь iterate — это именно функция, ведь она вычисляет вполне конкретный результат.
Подобные функции называются генераторными. Они строятся с использованием ключевого слова yield и возвращают объект-генератор.
Но где же числа? Их нам выдаст объект-генератор, который работает как итератор бесконечной последовательности в данном случае.
Обратите внимание на формулировку «работает как итератор». В Python многое работает на соглашениях, поэтому если что-то ведет себя как итератор, то оно и считается итератором.
Вот как можно применить полученную функцию:
Здесь уже вызывающая сторона решает, когда и сколько элементов ей нужно. При этом код генераторной функции не нагружен этим лишним для нее смыслом.
Инициализация, приостановка и завершение генерации
В коде выше ключевое слово yield очень похоже на return. Оно точно так же возвращает один элемент, а не генераторное выражение. Еще одно сходство заключается в том, что управление переходит обратно к коду, который запросил элемент у итератора.
Обычно return останавливает выполнение тела функции раз и навсегда. В отличие от него, yield выполнение приостанавливает. Выполнение возобновляется, когда вызывающая сторона попросит новый элемент посредством next(). Оно продолжается, пока не произойдет одно из этих событий:
- Встретится новый
yield - Встретится
return - Выполнится последняя строчка тела функции
В первом случае вызывающая сторона получит сгенерированное значение, а выполнение вновь приостановится. Остальные два случая работают одинаково — они завершают процесс итерации.
Код, который находится выше самого первого yield, часто называют кодом инициализации. Он выполняется, когда к объекту-генератору впервые применяют next().
Во время фаз инициализации и завершения удобно открывать файлы, содержимое которых будет порционно выдавать итератор, а потом своевременно этот файл закрыть. Декларативные генераторы такой возможности не имеют сами по себе, так что хотя бы ради этой гибкости стоит уметь писать генераторные функции.
Рассмотрим небольшой пример, сообщающий обо всех фазах своей работы:
Этот пример демонстрирует, что объект-генератор реагирует на iter(), но при этом не может быть использован повторно. Впрочем, новый экземпляр всегда можно получить, вызвав генераторную функцию. Зато сохранение состояния между несколькими участками, потребляющими элементы итератора бывает очень полезно.