MyTetra Share
Делитесь знаниями!
Понимание работы ключевого слова yield и самописных генераторов в Python
Время создания: 08.09.2025 17:49
Автор: xintrea
Текстовые метки: python, генератор, yield, функция, ленивые, вычисления, range, break, return, next(), close(), throw(), send()
Раздел: Компьютер - Программирование - Язык Python
Запись: xintrea/mytetra_syncro/master/base/17573429663bxq2e1u2m/text.html на raw.githubusercontent.com

Введение


Программисты на языке Python обычно хорошо понимают концепцию "готовых" генераторов. Нет никаких проблем в том, чтобы понять, что такое простейший генератор range() и как с ним работать через конструкцию for ... in:



for i in range(3):

print(i)



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



Итак, следует понимать следующую вещь. Если функция возвращает значение через return, то это обычная функция. Вызов обычной функции возвращает некое значение функции. Тип возвращаемого значения функции будет таким, который имеется у возвращаемого значения. Это основы того, как работает функция в Python. Здесь ничего нового нет.


Если же в функции вместо return используется yield, то такая функция становится генератором. И вызов этой функции будет возвращать специализированный объект типа "генератор". И работа с результатом вызова функции, в которой имеется yield, должна происходить в коде на Python не как с классическим возвращаемым значением, а как с объектом-генератором.



Путанницу здесь добавляет тот факт, что и обычная функция, и генератор объявляются в Python как обычная функция:



# Это функция, которая возвращает список

def with_return():

return [1, 2, 3]



# Это генератор, который учавствует в генерации последовательности чисел

def with_yield():

yield 1

yield 2

yield 3



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



Создание генератора и обращение к генератору


При работе с генератором надо четко понимать и разделять сам момент создания генератора и моменты обращения к генератору.



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



Создание генератора происходит в тот момент, когда в тексте программы встречается имя генератора, завершаемое круглыми скобками. В скобках могут находиться параметры, передаваемые генератору. В момент создания генератора происходит создание специального объекта-генератора. Запуск первого обращения к генератору в этот момент не происходит! Производится только создание объекта генератора и все.



Что такое объект генератора?


Объект генератора - это специальная структура в языке Python, которая тесно связана с кодом, исполняемым внутри генератора. Это специальная "обвязка" вокруг кода генератора, и она способна запускать и приостанавливать код генератора. Когда происходит обращение к генератору, код генератора запускается от своей последней рабочей точки. И код генератора приостанавливается, когда встречается ключевое слово yield. В момент остановки по ключевому слову yield, значение, указанное в yield, передается во внешний код, который делал обращение к генератору.



Обращение к генератору представляет собой обращение к объекту генератора, чтобы он произвел запуск кода генератора от предыдущей "точки останова" генератора до первого сработавшего yield. Если происходит самый первый запуск кода генератора, то предыдущей "точкой останова" является начало кода генератора.


Результатом обращения к генератору является значение, которое было выдано сработавшим ключевым словом yield.



Важно понимать: код внутри генератора именно "приостанавливается" при срабатывании yield, и продолжает свою работу в момент следующего обращения к генератору. Причем продолжение работы происходит со следующей команды, которая написана после последнего сработавшего yield. Не с начала кода итератора, а именно со следующей за yield комады!



Обращение к генератору производится с помощью системной функции next(), которая имеется в Python. В качестве параметра, в эту функцию передается объект генератора. Часто, в конструкциях языка Python вызов получения очередного значения как бы "скрыт", никакого вызова next() или других подобных функций в коде не видно, но по-факту они происходят. Например, так сделано в конструкции for...in, которая была упомянута в самом начале этой статьи.



Примеры


Вот пример с явным использованием функции next():



def number_generator():

yield 1

yield 2

yield 3


g = number_generator() # Создание генератора


print( next( g ) ) # Обращение к генератору

print( next( g ) ) # Обращение к генератору

print( next( g ) ) # Обращение к генератору



Результат:



1

2

3



Функция next(), на самом деле запускает внутри себя метод __next__(), который обязательно имеется у объекта-генератора g (который, как видно выше, передается в функцию next() как параметр). То есть, можно писать получение значений не через системную функцию next(), а через вызов метода g.__next__(), и такой код тоже будет полностью рабочим:



g = number_generator()


print( g.__next__() )

print( g.__next__() )

print( g.__next__() )



А вот классический пример с неявным использованием __next__() внутри цикла for...in:



def number_generator():

yield 1

yield 2

yield 3


for i in number_generator():

print( i )



Результат тот же:



1

2

3



Генератор в конструкции for...in


В конструкции for...in, на самом деле, происходит много чего "скрытого":



Во-первых, происходит создание генератора, как результат работы имя_генератора() внутри строки "for i in имя_генератора():"


Во-вторых, циклично, происходит обращение к этому генератору с помощью интерфейса итерируемых объектов. В Python объект генератора, по своей сути, является итерируемым объектом, и реализует интерфейс итерируемого объекта. Наличие метода __next__() является следствием того, что у объекта генератора наличествует интерфейс итерируемого объекта. И в цикле for...in происходит постоянный вызов метода __next__() генератора, который возвращает очередное значение.


В третьих, внутри for...in происходит отслеживание появления у генератора исключения StopIteration. Это исключение возникает тогда, когда код генератора дошел до последней инструкции, и дальше выполняться просто нечему. То же самое будет происходить, если в генераторе встретится ключевое слово return без параметров.



Единственное законое использование ключевого слова return в генераторе - использование его для обозначения того, что генератор завершил свою работу. Обычно, слово return используется в условии, при срабатывании которого генератор должен завершить генерацию последовательности.



def number_generator():

n = 1

while True:


# Условие остановки генератора -

# число делится на 7 без остатка

# и это не само число 7

if n % 7 == 0 and n != 7:

return


yield n

n += 1


for num in number_generator():

print(num)



Данный код напечатает значения от 1 до 13. Значение 14 сгенерировано не будет, так как на нем генерация останавливается и до выполнения yield со значением 14 дело не доходит.



Остановка генератора внешним кодом


Как уже стало понятно, генератор может сам остановить свою работу, если генерируемая последовательность закончилась.


Однако, остановить генератор можно и внешним кодом, используя метод генератора close():



def infinite_gen():

n = 0

while True:

yield n

n += 1


gen = infinite_gen()


print(next(gen)) # 0

print(next(gen)) # 1


gen.close() # Генератор закрыт


# next(gen) # Будет ошибка StopIteration



Здесь надо обратить внимание на два момента.


Первое - генератор при вызове метода close() именно закрывается, т. е. переходит в состояние, как будто он полностью отработал, и больше не генерирует значений. Он не сбрасывается в начальное состояние, а именно закрывается. При этом высвобождаются все ресурсы, используемые объектом-генератором. Объект генератора - это "одноразовый" объект, и повторно его использовать нельзя.


Если нужно повторно использовать генератор - надо заново создать объект генератора и использовать уже новый объект. Благо, что из-за концепции отложенных вычислений, создание и использование объекта генератора - это достаточно низкозатратная задача.



В Python имеются способы "перезапуска" объекта генератора, но они достаточно сложны. Например, вместо написания генератора через конструкцию "def имя_итератора():" можно вручную создать класс итератора, в котором надо реализовать обязательные методы __init__(), __iter__(), __next__(), и помимо них сделать метод reset(), в котором, к примеру, сделать обнуление счетчика. Далее, во внешнем коде, у объекта генератора можно вызывать метод reset(), тем самым перезапуская генератор.



Второе - исторически так сложилось, что в Python, для работы с генераторами, имеется системная функция next(), но нет системной функции close(). Для закрытия итератора надо вызывать метод объекта итератора close(), а не системную фукнцию close(). Это видно в вышеприведенном примере. То же самое касается методов генератора send() и throw() (см. далее).



Отправка значений в генератор во время его работы


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



x = yield n



или, если одномоментно нужно получить несколько значений:



a, b, c = yield n



Чтобы понять, что здесь произойдет, надо учесть, что и сам вызов очередной итерации должен делаться не через системную функцию next(), а через метод генератора send(). В качестве параметров метода send() указываются значения, которые надо передать в генератор.



g = example_generator()


print( g.send(100) )

print( g.send(1, 2, 3) )



Важно, чтобы количество передаваемых параметров на текущей итерации соответствовало количеству принимаемых параметров, прописанных у текущего yield.


Как работает send()? Визуально, можно представить так. Предположим, что внутри генератора написана строка:



a, b, c = yield n



Работа предыдущей итерации завершается в правой части этой строки, где написано yield n. Новая итерация запускается в момент, когда во внешнем коде возникает команда g.send(1, 2, 3). Этот вызов приводит к заполнению значений a, b, c, перечисленных в левой части вышеуказанной строки генератора. Далее код итератора выполняется с учетом того, что переменные a, b, c существуют и имеют какие-то значения. И когда будет достигнут следующий yield, код остановится, а результат yield будет вычислен с учетом наличия этих данных. Т. е. в ответ на вышеуказанную команду g.send(1, 2, 3) от генератора придет значение, в котором как-то учитываются эти переданные параметры.




Дописать...




Так же в этом разделе:
 
MyTetra Share v.0.67
Яндекс индекс цитирования