Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Функции-генераторы Python: Декларативное программирование

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

Проще говоря, иногда нужно спуститься на такой уровень, на котором выдача элементов наружу была так же проста и контролируема, как вывод элементов на печать с помощью print(). Да, код будет выглядеть очень императивно, но зато он будет эффективным!

А поскольку в Python сама концепция итеративных вычислений прослеживается повсеместно, то и средства низкоуровневого программирования потоков данных встроены в сам язык.

Ключевое слово yield

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

def iterate(x0, m):
    x = x0
    while True:
        print(x)
        x *= m

iterate(1, 1.1)

print("impossible!")

Как только процедура iterate — а это именно процедура — будет вызвана, то все возрастающие числа будут выводиться бесконечно, ведь никакого завершения цикла мы не предусмотрели. А так как выполнение процедуры iterate никогда не завершится, то и весь код, следующий за вызовом (iterate(1, 1.1)) не будет выполнен никогда!

Если же последовательность была бы нам нужна за пределами процедуры iterate, то мы даже не сможем сделать return вместо print(), ведь это приведет к остановке процесса генерации. Мы могли бы передать в виде аргумента список, в который процедура бы добавляла элементы вместо вывода на печать. Однако использовать список мы не сможем: ведь процедура никогда не завершится! Разве что мы как-то ограничим количество элементов извне. Но далеко не всегда можно заранее узнать, сколько итераций нужно выполнить. Что приводит нас к переносу логики ограничения последовательности внутрь самой процедуры. Получается, что саму идею бесконечной последовательности нам не выразить?

Здесь мы и применим новое ключевое слово yield:

def iterate(x0, m):
    x = x0
    while True:
        yield x  # вместо print()
        x *= m
iterate(1, 1.1)
# <generator object iterate at 0x...>

Заметьте, что вызов функции iterate вычислился в некий <generator object>, сама же функция не зациклилась! Да, теперь iterate это именно функция, ведь она вычисляет вполне конкретный результат!

Функции, которые построены с использованием ключевого слова yield и возвращающие <generator object> называют генераторными функциями (generator functions).

Но где же числа? Их нам выдаст <generator object>, который работает как итератор, причем в данном случае это итератор бесконечной последовательности.

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

Вот как полученную функцию можно применить:

for n in iterate(1, 1.2):
    print(n)
    if n > 3:
        break

# => 1
# => 1.2
# => 1.44
# => 1.728
# => 2.0736
# => 2.48832
# => 2.9859839999999997
# => 3.5831807999999996

Здесь уже вызывающая сторона решает, когда и сколько элементов ей нужно. Код же генераторной функции не нагружен этим лишним для нее смыслом.

Инициализация, приостановка и завершение генерации

Ключевое слово yield в коде выше очень похоже на return: оно точно так же "возвращает" один элемент, а не, скажем, generator expression. Еще одно сходство заключается в том, что управление переходит обратно к коду, который элемент у итератора запросил.

Но там, где return останавливает выполнение тела функции раз и навсегда, yield выполнение приостанавливает. Возобновляется же выполнение тогда, когда вызывающая сторона попросит новый элемент посредством next(), и продолжается, пока не произойдет одно из:

  • встретится новый yield
  • встретится return
  • выполнится последняя строчка тела функции

В первом случае вызывающей стороне будет предоставлено сгенерированное значение, а выполнение вновь приостановится. Остальные два случая работают одинаково — завершают процесс итерации.

Код же, который находится выше самого первого yield часто называют кодом инициализации. Он выполняется в тот момент, когда к generator object применяют самый первый next(), но не ранее!

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

Рассмотрим маленький пример, сообщающий обо всех фазах своей работы:

def f():
    print('Initializing...')
    yield 'one'
    print('Continue...')
    yield 'two'
    print('Stopping...')

i = f()
# еще ничего не выполнялось!
i
# <generator object f at 0x...>

next(i)  # самый первый next()
# => Initializing...
# => 'one'
# прошла инициализация и получено первое значение

next(i)
# => Continue...
# => 'two'
# выполнился код между первым yield и следующим, получено значение №2

next(i)
# => Stopping...
# Traceback (most recent call last):
#   ...
#     next(i)
# StopIteration
# выполнение дошло до конца тела функции, итерация завершена

j = iter(i)  # пробуем получить новый итератор, вдруг получится?
j is i
# True
# iter() просто вернул ссылку на оригинальный объект

next(j)
# Traceback (most recent call last):
#   ...
#     next(j)
# StopIteration
# Увы, повторно ту же последовательность не обойти

Этот пример заодно демонстрирует, что каждый generator object, хоть и реагирует на iter(), повторно быть использован не может. Впрочем, новый экземпляр всегда можно получить, вызвав генераторную функцию. Зато сохранение состояния между несколькими участками, потребляющими элементы итератора бывает очень полезно!


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты.

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы Python-разработчик
Профессия
с нуля
Разработка веб-приложений на Django
1 декабря 10 месяцев

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и соглашаетесь с «Условиями использования»