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




Дописать...


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