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

Предположим, у нас имеется вот такая функция:

def getAllAverage(N):
    avs = []
    count = 0
    S = 0
    for i in range(1,N+1):
        count += 1
        S += i
        avs.append( S/count )
 
    return avs

которая вычисляет средние арифметические для каждой из сумм: для одного значения, для двух, трех и так до N. При вызове этой функции:

print( getAllAverage(100) )

получим довольно внушительный список из чисел. В памяти он занимает:

print(getAllAverage(100).__sizeof__() )

888 байт. Конечно, это относительно немного. Но представьте, что будет, если N увеличить в десятки раз. Мы получим список, занимающий мегабайты памяти компьютера! Можно ли здесь как то оптимизировать этот процесс по размеру занимаемой памяти? Да, для подобных задач в Python предусмотрены так называемые функции-генераторы. Давайте для начала запишем простую функцию-генератор, а потом вернемся к нашей исходной задаче.

Пусть имеется функция, возвращающая обычный список из чисел:

def f():
    return list(range(10))
print( f() )

Превратим ее в функцию-генератор:

def f():
    for x in range(10):
        yield x

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

Для вызова функции-генератора присвоим ее текущее состояние некоторой переменной:

s = f()

Эта переменная будет ссылкой на генератор, то есть, по сути, являться итератором:

print( s )

и мы уже к ней можем применить функцию next для перебора значений, которые будет возвращать функция:

print( next(s) )
print( next(s) )
print( next(s) )

Причем, мы здесь сразу получаем все преимущества итераторов, а именно – экономию занимаемой памяти данными.

Теперь вернемся к нашей исходной задаче и превратим нашу первую функцию в генератор:

def getAllAverage(N):
    count = 0
    S = 0
    for i in range(1,N+1):
        count += 1
        S += i
        yield S/count

и вызовем ее:

it = getAllAverage(10)
print( next(it) )
print( next(it) )
print( next(it) )
print( next(it) )
print( next(it) )

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

Задание для самоподготовки

Пусть дан текст:

t = """Генератор – это итератор, элементы которого
можно перебирать (итерировать) только один раз.
Итератор – это объект, который поддерживает функцию next()
для перехода к следующему элементу коллекции."""

Написать функцию-генератор для выделения слов из этого текста (слова разделяются пробелом, либо переносом строки ‘\n’). Список всех слов при этом в функции не создавать.

Видео по теме