У декларативного подхода к описанию последовательностей много достоинств. Но иногда возникает необходимость поступиться декларативностью и применить императивные приёмы вроде изменяемого состояния или возможности досрочно прервать процесс генерации. А ещё возникают ситуации, когда элементы выходной последовательности зависят друг от друга или от элементов входной последовательности не настолько явно, чтобы можно было обойтись "формулой".
Проще говоря, иногда нужно спуститься на такой уровень, на котором выдача элементов наружу была так же проста и контролируема, как вывод элементов на печать с помощью 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()
, повторно быть использован не может. Впрочем, новый экземпляр всегда можно получить, вызвав генераторную функцию. Зато сохранение состояния между несколькими участками, потребляющими элементы итератора бывает очень полезно!
Вам ответят команда поддержки Хекслета или другие студенты.
Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.
Загляните в раздел «Обсуждение»:
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.
Наши выпускники работают в компаниях:
С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.
Зарегистрируйтесь или войдите в свой аккаунт