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

Источники:

  • Саммерфилд, Глава 4 Управляющие структуры и функции / Собственные функции / Лямбда-функции
  • Саммерфилд, Глава 8 Усовершенствованные приемы программирования / Выражения-генераторы и функции-генераторы

Зачем нужны генераторы?

Это средство отложенных вычислений. Значения вычисляются только тогда, когда они действительно необходимы.

Удобнее, чем вычислить за один раз огромный список и потом держать его в памяти.

Некоторые генераторы могут воспроизводить столько значений, сколько потребуется, без ограничения сверху. Например, последовательность квадратов 1, 4, 9, 16 и так далее.

Термины

Функция-генератор, или метод-генератор – это функция или метод, содержащая выражение yield. В результате обращения к функции-генератору возвращается итератор. Значения из итератора извлекаются по одному, с помощью его метода next(). При каждом вызове метода next() он возвращает результат вычисления выражения yield. (Если выражение отсутствует, возвращается значение None.) Когда функция-генератор завершается или выполняет инструкцию return, возбуждается исключение StopIteration. На практике очень редко приходится вызывать метод next() или обрабатывать исключение StopIteration. Обычно функция-генератор используется в качестве итерируемого объекта.

Список vs генератор

Создает и возвращает список:

def letter_range(a, z):
    res = []
    while ord(a) < ord(z):
        res.append(a)
        a = chr(ord(a)+1)
    return res

Использование:

for c in letter_range('m', 'v'):    # одинаково для списка и генератора
    print(c)

az = letter_range('m', 'v')         # az - список

Создает и возвращает генератор, т.е. возвращает каждое значение по требованию:

def letter_range(a, z):
    while ord(a) < ord(z):
        yield a
        a = chr(ord(a)+1)

Использование:

for c in letter_range('m', 'v'):    # одинаково для списка и генератора
    print(c)

az = letter_range('m', 'v')         # az - генератор
az_1 = list(az)                     # список
az_2 = tuple(az)                    # кортеж

Выражения-генераторы

Похожи на генераторы списков, но пишем круглые скобки, а не квадратные:

(expression for item in iterable)
(expression for item in iterable if condition)

Напишем функцию, которая для словаря возвращает генератор, который выдает пары ключ-значение в порядке, в котором отсортированы ключи. Вариант 1:

def items_in_key_order(d):
    for key in sorted(d):
        yield (key, d[key])

Вариант 2: (эквивалентно)

def items_in_key_order(d):
    return ((key, d[key]) for key in sorted(d))

Вариант 3: теперь через лямбда-фукнцию:

items_in_key_order = lambda d: ((key, d[key]) for key in sorted(d))

Использование:

>>> d1 = {12:21, 3:3, -6:6, 100:0}
>>> d1
{12: 21, 3: 3, -6: 6, 100: 0}
>>> for k, v in items_in_key_order(d1): print(k, v) # ключи отсортированы
...
-6 6
3 3
12 21
100 0
>>> for k, v in d1.items(): print(k, v)             # несортированный порядок
...
12 21
3 3
-6 6
100 0

Бесконечные последовательности

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

def quarters(next_quarter=0.0):
    while True:
        yield next_quarter
        next_quarter += 0.25

Получим с ее помощью список элементов от 0 до 1: [0.0, 0.25, 0.5, 0.75, 1.0]

result = []
for x in quarters():
    result.append(x)
    if x >= 1.0:
        break

Хотим, чтобы последовательность пропустила "плохие числа" (0.5 и далее) и перешла сразу к "хорошим" (1). Изменим генератор.

def quarters(next_quarter=0.0):
    while True:
        received = (yield next_quarter)   # было  yield next_quarter
        if received is None:              # вдруг фигню подсунут, проигнорируем
            next_quarter += 0.25
        else:                             # нефигню сделаем следующим значением
            next_quarter = received

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

result = []
generator = quarters()
while len(result) < 5:
    x = next(generator)
    if abs(x - 0.5) < sys.float_info.epsilon:
        x = generator.send(1.0)
    result.append(x)

print(result)    # [0.0, 0.25, 1.0, 1.25, 1.5].

Здесь создается переменная, хранящая ссылку на генератор, и вызывается встроенная функция next(), которая извлекает очередной элемент из указанного ей генератора. (Того же эффекта можно было бы достичь вызовом специального метода next() генератора, в данном случае следующим образом: x = generator.next().) Если значение равно 0.5, генератору передается значение 1.0 (которое немедленно возвращается обратно).

Лутц, стр 572: В версии Python 2.5 в протокол функций-генераторов был добавлен метод send. Метод send не только выполняет переход к следующему элементу в последовательности результатов, как это делает метод next, но еще и обеспечивает для вызывающей программы способ взаимодействия с генератором, влияя на его работу. С технической точки зрения yield в настоящее время является не инструкцией, а выражением, которое возвращает элемент, передаваемый методу send (несмотря на то, что его можно использовать любым из двух способов – как yield X или как A = (yield X)). Когда выражение yield помещается справа от оператора присваивания, оно должно заключаться в круглые скобки, за исключением случая, когда оно не является составной частью более крупного выражения. Например, правильно будет написать X = yield Y, а также X = (yield Y) + 42. При использовании расширенного протокола значения передаются генератору G вызовом метода G.send(value). После этого программный код генератора возобновляет работу, и выражение yield возвращает значение, полученное от метода send. Когда вызывается обычный метод G.next() (или выполняется эквивалентный вызов next(G)), выражение yield возвращает None.

results matching ""

    No results matching ""