🐍 Что такое yield в Python? Самый популярный вопрос на Стаковерфлоу по Питону

Самый популярный Python-вопрос на Stackoverflow связан с ключевым словом yield. Разберемся с его назначением и особенностями использования.
🐍 Что такое yield в Python? Самый популярный вопрос на Стаковерфлоу по Питону

Официальная документация 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 <= max_dist and distance >= min_dist:
        result.extend(node._values)
        candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
        return result
    

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

А вот и лучший ответ на вопрос о yield в Python:

Для понимания того, что делает 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, являются итерируемыми – списки, строки, файлы. Итерируемые объекты очень удобны, потому что они не ограничивают количество повторных считываний данных. Однако вся информация находится в оперативной памяти, и при большом объеме данных это нежелательно.

Генераторы

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

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

    

Генераторы используются так же, как и списковые включения, отличие заключается в применении круглых скобок () вместо квадратных []. Кроме того, в отличие от списка, сгенерированного с помощью спискового включения, к генератору нельзя обратиться повторно – вычисляя каждый последующий элемент, генератор «забывает» о предыдущем.

yield

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

        >>> def create_generator():
...    mylist = range(3)
...    for i in mylist:
...        yield i*i
...
>>> mygenerator = create_generator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object create_generator at 0xb7555c34>
>>> for i in mygenerator:
...     print(i)
0
1
4

    

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

Для эффективного использования нужно понимать главную особенность yield: при вызове функции код в теле функции не исполняется. Функция просто возвращает объект-генератор. Код вызывается каждый раз, когда for обращается к генератору. При первом запуске функции она будет исполняться, пока не дойдет до yield, после чего вернет первое значение из цикла. При каждом последующем вызове будет происходить следующая итерация и возвращение значения цикла. Процесс будет повторяться, пока генератор не окажется пустым. Генератор считается пустым, если функция не встречает yield – это происходит либо в конце цикла, либо при невыполнении условий if и else.

Пояснение кода, приведенного в вопросе

Генератор:

        # Создание метода узла, который будет возвращать генератор
def _get_child_candidates(self, distance, min_dist, max_dist):
  # Код будет вызываться при каждом обращении к объекту-генератору
  # Если слева от узла есть потомок
  # И расстояние соответствует условию, yield вернет этого потомка
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild
 
  # Если есть потомок справа от узла
  # И расстояние соответствует условию, yield вернет этого потомка
    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 <= max_dist and 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)) завершит использование всех значений генератора, но цикл while не остановится на этом, а продолжит процесс создания новых объектов-генераторов, поставляющих значения, отличающиеся от предыдущих (потому что они относятся к другим узлам).

В коде используется метод extend(), который принимает итерируемые объекты и добавляет их к списку. Метод extend() обычно используется для добавления в список другого списка:

        >>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]

    

Однако в рассматриваемом коде extend() принимает не список, а генератор, что значительно оптимизирует программу:

  • отпадает необходимость повторного считывания данных;
  • не нужно хранить в памяти множество потомков.

Метод extend() может добавлять в список любые итерируемые объекты – генераторы, строки, кортежи, списки. Это называется утиной типизацией.

Где еще пригодится yield

Использование yield решает проблему перегрузки памяти при работе с объемными файлами. К примеру, считывание объемных csv-файлов часто приводит к зависанию и прерыванию программы с ошибкой MemoryError. Чтобы не загружать в память весь файл сразу, и считывать только нужные строки, применяется yield. Этот код, к примеру, вернет количество строк в файле:

        def csv_reader(file_name):
    for row in open(file_name, "r"):
        yield row
    

С помощью yield можно сгенерировать бесконечную (в отличие от range) последовательность чисел:

        def infinite_sequence():
    num = 0
    while True:
        yield num
        num += 1
    

Выполнение этого кода будет продолжаться до ручного прерывания.

Заключение

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

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

***

Материалы по теме




Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ