Yield generators in python

№30 Генераторы / для начинающих

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

Отдельное внимание будет уделено инструкции yield . Она является частью генератора и заменяет ключевое слово return . Когда программа доходит до yield , то функция переходит в состояние ожидания и продолжает работу с того же места при повторном вызове.

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

Генератор предоставляет способ создания итераторов, решая следующую распространенную проблему.

Создание итератора в Python — достаточно громоздкая операция. Для этого нужно написать класс и реализовать методы __iter__() и __next__() . После этого требуется настроить внутренние состояния и вызывать исключение StopIteration , когда больше нечего возвращать.

Как создать генератор в Python?

Генератор — это альтернативный и более простой способ возвращать итераторы. Процедура создания не отличается от объявления обычной функции.

Есть два простых способа создания генераторов в Python.

Функция генератора

Генератор создается по принципу обычной функции.

Отличие заключается в том, что вместо return используется инструкция yield . Она уведомляет интерпретатор Python о том, что это генератор, и возвращает итератор.

Синтаксис функции генератора:

Источник

Как работает yield

На StackOverflow часто задают вопросы, подробно освещённые в документации. Ценность их в том, что на некоторые из них кто-нибудь даёт ответ, обладающий гораздо большей степенью ясности и наглядности, чем может себе позволить документация. Этот — один из них.

Как используется ключевое слово yield в Python? Что оно делает?

Например, я пытаюсь понять этот код (**):

def _get_child_candidates(self, distance, min_dist, max_dist): if self._leftchild and distance - max_dist < self._median: yield self._leftchild if self._rightchild and distance + max_dist >= self._median: yield self._rightchild 
result, candidates = list(), [self] while candidates: node = candidates.pop() distance = node._get_dist(obj) if distance = min_dist: result.extend(node._values) candidates.extend(node._get_child_candidates(distance, min_dist, max_dist)) return result 

Что происходит при вызове метода _get_child_candidates? Возвращается список, какой-то элемент? Вызывается ли он снова? Когда последующие вызовы прекращаются?

Читайте также:  Html div по ширине экрана

** Код принадлежит Jochen Schulz (jrschulz), который написал отличную Python-библиотеку для метрических пространств. Вот ссылка на исходники: http://well-adjusted.de/~jrschulz/mspace/

Итераторы

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

>>> mylist = [1, 2, 3] >>> for i in mylist : . print(i) 1 2 3 

Mylist является итерируемым объектом. Когда вы создаёте список, используя генераторное выражение, вы создаёте также итератор:

>>> mylist = [x*x for x in range(3)] >>> for i in mylist : . print(i) 0 1 4 

Всё, к чему можно применить конструкцию «for… in. », является итерируемым объектом: списки, строки, файлы… Это удобно, потому что можно считывать из них значения сколько потребуется — однако все значения хранятся в памяти, а это не всегда желательно, если у вас много значений.

Генераторы

Генераторы это тоже итерируемые объекты, но прочитать их можно лишь один раз. Это связано с тем, что они не хранят значения в памяти, а генерируют их на лету:

>>> mygenerator = (x*x for x in range(3)) >>> for i in mygenerator : . print(i) 0 1 4 

Всё то же самое, разве что используются круглые скобки вместо квадратных. НО: нельзя применить конструкцию for i in mygenerator второй раз, так как генератор может быть использован только единожды: он вычисляет 0, потом забывает про него и вычисляет 1, завершаяя вычислением 4 — одно за другим.

Yield

Yield это ключевое слово, которое используется примерно как return — отличие в том, что функция вернёт генератор.

>>> def createGenerator() : . mylist = range(3) . for i in mylist : . yield i*i . >>> mygenerator = createGenerator() # создаём генератор >>> print(mygenerator) # mygenerator является объектом! >>> for i in mygenerator: . print(i) 0 1 4 

В данном случае пример бесполезный, но это удобно, если вы знаете, что функция вернёт большой набор значений, который надо будет прочитать только один раз.

Чтобы освоить yield, вы должны понимать, что когда вы вызываете функцию, код внутри тела функции не исполняется. Функция только возвращает объект-генератор — немного мудрёно 🙂

Ваш код будет вызываться каждый раз, когда for обращается к генератору.

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

Читайте также:  text-align

Генератор считается пустым, как только при исполнении кода функции не встречается yield. Это может случиться из-за конца цикла, или же если не выполняется какое-то из условий «if/else».

Объяснение кода из исходного вопроса

# Создаём метод узла, который будет возвращать генератор def _get_child_candidates(self, distance, min_dist, max_dist): # Этот код будет вызываться при каждом обращении к объекту-генератору: # Если у узла есть потомок слева # И с расстоянием всё в порядке, возвращаем этого потомка if self._leftchild and distance - max_dist < self._median: yield self._leftchild # Если у узла есть потомок справа # И с расстоянием всё в порядке, возвращаем этого потомка if self._rightchild and distance + max_dist >= self._median: yield self._rightchild # Если исполнение дошло до этого места, генератор считается пустым 
# Создаём пустой список и список со ссылкой на текущий объект result, candidates = list(), [self] # Входим в цикл по кандидатам (в начале там только один элемент) while candidates: # Вытягиваем последнего кандидата и удаляем его из списка node = candidates.pop() # Вычисляем расстояние между объектом и кандидатом distance = node._get_dist(obj) # Если с расстоянием всё в порядке, добавляем в результат if distance = min_dist: result.extend(node._values) # Добавляем потомков кандидата в список кандидатов, # чтобы цикл продолжал исполняться до тех пор, # пока не обойдёт всех потомков потомков кандидата candidates.extend(node._get_child_candidates(distance, min_dist, max_dist)) return result 
  • Цикл итерируется по списку, но списко расширяется во время итерации 🙂 Это лаконичный способ обойти все сгрупиррованные данные, зоть это и немного опасно, так как может обернуться бесконечным циклом. В таком случае candidates.extend(node._get_child_candidates(distance, min_dist, max_dist)) исчерпает все значения генератора, но при этом продолжит создавать новые объекты-генераторы, которые будут давать значения, отличные от предыдущих (поскольку применяются к к другим узлам).
  • Метод extend() это метод объекта списка, который ожидает на вход что-нибудь итерируемое и добавляет его значения к списку.
>>> a = [1, 2] >>> b = [3, 4] >>> a.extend(b) >>> print(a) [1, 2, 3, 4] 
  • Нет необходимости читать значения дважды.
  • Может случиться так, что потомков много и хранить их всех в памяти не хочется.

Читатель может остановиться здесь, или же прочитать ещё немного о продвинутом использовании генераторов:

Контроль за исчерпанием генератора

>>> class Bank(): # создаём банк, строящий торговые автоматы (ATM — Automatic Teller Machine) . crisis = False . def create_atm(self) : . while not self.crisis : . yield "$100" >>> hsbc = Bank() # когда всё хорошо, можно получить сколько угодно денег с торгового автомата >>> corner_street_atm = hsbc.create_atm() >>> print(corner_street_atm.next()) $100 >>> print(corner_street_atm.next()) $100 >>> print([corner_street_atm.next() for cash in range(5)]) ['$100', '$100', '$100', '$100', '$100'] >>> hsbc.crisis = True # пришёл кризис, денег больше нет! >>> print(corner_street_atm.next()) >>> wall_street_atm = hsbc.create_atm() # что верно даже для новых автоматов >>> print(wall_street_atm.next()) >>> hsbc.crisis = False # проблема в том, что когда кризис прошёл, автоматы по-прежнему пустые. >>> print(corner_street_atm.next()) >>> brand_new_atm = hsbc.create_atm() # но если построить ещё один, будешь снова в деле! >>> for cash in brand_new_atm : . print cash $100 $100 $100 $100 $100 $100 $100 $100 $100 . 

Это может оказаться полезным для разных целей вроде управления доступом к какому-нибудь ресурсу.

Читайте также:  Css element by title

Ваш лучший друг Itertools

Модуль itertools содержит специальные функции для работы с итерируемыми объектами. Желаете продублировать генератор? Соединить два генератора последовательно? Сгруппировать значения вложенных списков в одну строчку? Применить map или zip без создания ещё одного списка?

Просто добавьте import itertools.

Хотите пример? Давайте посмотрим на возможные порядки финиширования на скачках (4 лошади):

>>> horses = [1, 2, 3, 4] >>> races = itertools.permutations(horses) >>> print(races) >>> print(list(itertools.permutations(horses))) [(1, 2, 3, 4), (1, 2, 4, 3), (1, 3, 2, 4), (1, 3, 4, 2), (1, 4, 2, 3), (1, 4, 3, 2), (2, 1, 3, 4), (2, 1, 4, 3), (2, 3, 1, 4), (2, 3, 4, 1), (2, 4, 1, 3), (2, 4, 3, 1), (3, 1, 2, 4), (3, 1, 4, 2), (3, 2, 1, 4), (3, 2, 4, 1), (3, 4, 1, 2), (3, 4, 2, 1), (4, 1, 2, 3), (4, 1, 3, 2), (4, 2, 1, 3), (4, 2, 3, 1), (4, 3, 1, 2), (4, 3, 2, 1)] 

Понимание внутреннего механизма итерации

Итерация это процесс, включающий итерируемые объекты (реализующие метод __iter__()) и итераторы (реализующие __next__()). Итерируемые объекты это любые объекты, из которых можно получить итератор. Итераторы это объекты, позволяющие итерировать по итерируемым объектам.

Больше информации по данному вопросу доступно в статье про то, как работает цикл for.

Источник

Оцените статью