Функция-генератор. Оператор yield

Курс по Python: https://stepik.org/course/100707

На этом занятии мы с вами поговорим о функциях-генераторах. Но вначале вернемся к выражениям-генераторам, которые рассматривали на предыдущем занятии. Давайте предположим, что нам нужны средние арифметические значения для разных последовательностей целых чисел:

1, 2, 3, 4, 5, 6, 7, 8, 9, 10
2, 3, 4, 5, 6, 7, 8, 9, 10
3, 4, 5, 6, 7, 8, 9, 10
4, 5, 6, 7, 8, 9, 10
5, 6, 7, 8, 9, 10
6, 7, 8, 9, 10
7, 8, 9, 10
8, 9, 10
9, 10

Для этого мы могли бы записать следующее выражение-генератор:

N = 10
a = (sum(range(i, N+1))/len(range(i, N+1)) for i in range(N))

Но оно не очень удобно для восприятия и редактирования и, кроме того, здесь дважды записана функция range() при вычислении среднего значения. Поправить это можно двумя способами. В первом, объявить собственную функцию для вычисления среднего арифметического и вызвать ее в генераторе:

def avg(start, stop, step=1):
    a = range(start, stop, step)
    return sum(a) / len(a)
 
 
N = 10
 
a = (avg(i, N + 1) for i in range(1, N))
print(list(a))

А во втором способе – создать собственную функцию-генератор, которая бы на выходе выдавала нужные значения. Давайте для начала запишем простую функцию-генератор, а потом вернемся к нашей исходной задаче. Функция будет просто возвращать значения списка:

def get_list():
    for x in [1, 2, 3, 4]:
        yield x

Смотрите, здесь в цикле записан новый для нас оператор yield, который возвращает текущее значение x и «замораживает» состояние функции до следующего обращения к ней (в том числе и все локальные переменные). Именно так определяются функции-генераторы. Если мы сейчас ее вызовем:

d = get_list()
print(d)

то, смотрите, переменная d ссылается на объект-генератор, то есть, мы здесь имеем дело с генератором, значения которого можно перебирать с помощью функции next():

print(next(d))
print(next(d))

Или, через цикл:

d = get_list()
for i in d:
    print(i, end=" ")

В этом и заключается роль оператора yield. Он превращает обычную функцию в генератор и при каждом вызове функции next() активизируется функция-генератор, возвращает очередное значение и «замораживает» свое состояние вместе с локальными переменными до следующего вызова функции next(). (Показываем это в режиме отладки).

Надеюсь, вы теперь хорошо представляете, как работает оператор yield и простая функция-генератор. Давайте вернемся к нашей исходной задаче и перепишем функцию avg() с использованием оператора yield (создаем еще одну):

def avg_gen(N, step=1):
    for i in range(1, N):
        a = range(i, N+1, step)
        yield sum(a) / len(a)

Принцип здесь тот же самый, на каждой итерации цикла будет возвращаться новое вычисленное среднее арифметическое значение. Далее, мы можем воспользоваться этой функцией-генератором и отобразить все ее значения, используя функцию list():

b = avg_gen(N)
print(list(b))

Как видите, результат полностью совпадает с первоначальным. То есть, мы заменили выражение-генератор на функцию-генератор. Ну и, как всегда, возникает вопрос, зачем это все было надо? Почему бы не пользоваться обычными генераторами? Что нам мешает это делать? В целом, ничего. Преимущество здесь, главным образом, в удобстве использования. В выражении генератора мы можем записать лишь один оператор для формирования значения, а в функции-генераторе – произвольный фрагмент программы, реализующий нужную нам логику формирования очередного значения. В этом ключевое отличие функции-генератора от обычного генератора.

В заключение этого занятия приведу еще один пример использования функции-генератора. Предположим, что мы хотим найти все начальные индексы слова «генератор» в текстовом файле lesson_54.txt. Для этого вначале откроем этот файл на чтение:

try:
    with open("lesson_54.txt", encoding="utf-8") as file:
        a = find_word(file, "генератор")
        print(list(a))
except FileNotFoundError:
    print("Файл не найден!")
except:
    print("Ошибка обработки файла!")

И внутри блока try вызовем функцию-генератор, которая и будет последовательно возвращать индексы найденного слова «генератор». Саму функцию определим выше (по программе), следующим образом:

def find_word(f, word):
    g_indx = 0
    for line in f:
        indx = 0
        while(indx != -1):
            indx = line.find(word, indx)
            if indx > -1:
                yield g_indx + indx
                indx += 1
 
        g_indx += len(line)

Мы здесь в первом цикле читаем файл по строкам. Во втором вложенном цикле while ищем указанное слово в строке, используя метод find(). И, если этот метод находит заданный фрагмент, то есть, возвращает значение больше -1, то функция генерирует на выходе значение индекса найденного слова как g_indx + indx. Здесь g_indx – это смещение по тексту для текущей строки, то есть, в ней мы суммируем длины предыдущих строк, чтобы сформировать индекс слова в тексте, а не в строке.

После запуска этой программы видим все найденные индексы данного слова по тексту. Как видите, мы в функции описали нетривиальную логику алгоритма поиска слова в текстовом файле. Сделать это с помощью обычного генератора было бы сложнее. В этом и преимущество функций-генераторов – они позволяют сразу, в одном месте кода, описывать нужный нам функционал.

На этом мы завершим с вами очередное занятие. Для закрепления материала, как всегда, пройдите практические задания и переходите к следующему уроку.

Курс по Python: https://stepik.org/course/100707

Видео по теме