Генератори

Генератори (англ. generators) — це спеціальний клас функцій, які спрощують завдання написання ітераторів. Звичайні функції обчислюють значення і повертають його, але генератори повертають ітератор, який повертає потік значень.

Ви безсумнівно знайомі з тим, як працюють звичайні виклики функцій у Python або C. Коли ви викликаєте функцію, вона отримує приватний простір імен, де створюються її локальні змінні. Коли функція досягає оператора return, локальні змінні знищуються, і значення повертається в точку виклику. Повторний виклик тієї ж функції створює новий приватний простір імен і новий набір локальних змінних. Але що, якби локальні змінні не знищувалися при виході з функції? Що, якщо ви могли б пізніше відновити виконання функції там, де вона зупинилася? Це те, що надають генератори; їх можна розглядати як функції, що відновлюються.

Ось найпростіший приклад генераторної функції:

def generate_ints(N):
   for i in range(N):
       yield i

Будь-яка функція, що містить ключове слово yield, є генераторною функцією; це визначається bytecode-компілятором Python, який спеціально компілює функцію в результаті.

Коли ви викликаєте генераторну функцію, вона не повертає одне значення; натомість вона повертає об'єкт генератора, який підтримує протокол ітератора. При виконанні виразу yield, генератор виводить значення i, подібно до оператора return. Велика різниця між yield і return полягає в тому, що при досягненні yield виконання генератора призупиняється, а локальні змінні зберігаються. При наступному виклику методу __next__(), функція відновить виконання.

Ось приклад використання генератора generate_ints():

def generate_ints(N):
   for i in range(N):
       yield i

gen = generate_ints(3)
print(gen)              # <generator object generate_ints at ...>
print(next(gen))        # 0
print(next(gen))        # 1
print(next(gen))        # 2

Ви також можете написати for i in generate_ints(5), або a, b, c = generate_ints(3).

Всередині генераторної функції, return value викликає StopIteration(value) при виклику методу __next__(). Коли це відбувається, або коли досягається кінець функції, обробка значень закінчується, і генератор не може повернути жодних додаткових значень.

Ви могли б досягти ефекту генераторів вручну, написавши свій власний клас і зберігаючи всі локальні змінні генератора як змінні екземпляра. Наприклад, повернення списку цілих чисел можна здійснити, встановивши self.count в 0, і метод __next__() збільшуватиме self.count і повертатиме його. Однак, для помірно складного генератора, написання відповідного класу може бути значно заплутанішим.

Тестовий набір, включений до бібліотеки Python, Lib/test/test_generators.py, містить ряд більш цікавих прикладів. Ось один генератор, що реалізує обхід дерева в порядку з використанням генераторів рекурсивно:

# Рекурсивний генератор, що генерує листя дерева в порядку.
def inorder(t):
    if t:
        for x in inorder(t.left):
            yield x

        yield t.label

        for x in inorder(t.right):
            yield x

Два інших приклади в test_generators.py дають рішення для задачі N-Ферзів (розміщення N ферзів на шаховій дошці NxN так, щоб жоден ферзь не загрожував іншому) і Турне Коня (знаходження маршруту, що дозволяє коню пройти кожну клітинку шахової дошки NxN без повторного відвідування жодної клітинки).

Передача значень у генератор

У Python 2.4 і раніше генератори тільки повертали значення. Після виклику коду генератора для створення ітератора не було способу передати будь-яку нову інформацію у функцію, коли її виконання відновлюється. Ви могли б створити цю можливість, змушуючи генератор дивитися на глобальну змінну або передаючи деякий змінний об'єкт, який викликаючи потім змінюють, але ці підходи заплутані.

У Python 2.5 існує простий спосіб передати значення в генератор. yield став виразом, повертаючи значення, яке може бути призначене змінній або іншим чином оброблене:

val = (yield i)

Я рекомендую завжди обгортати вираз yield у дужки, коли ви робите щось із поверненим значенням, як у наведеному вище прикладі. Дужки не завжди потрібні, але легше завжди їх додавати, ніж пам'ятати, коли вони потрібні.

PEP 342 пояснює точні правила, які є такими, що вираз yield завжди повинен бути в дужках, крім випадків, коли він знаходиться на верхньому рівні виразу на правій стороні присвоєння. Це означає, що ви можете написати val = yield i, але повинні використовувати дужки, коли є операція, як у val = (yield i) + 12.

Значення передаються в генератор шляхом виклику методу send(value). Цей метод відновлює код генератора, і вираз yield повертає вказане значення. Якщо викликається звичайний метод __next__.(), yield повертає None.

Ось простий лічильник, який збільшується на 1 і дозволяє змінювати значення внутрішнього лічильника.

def counter(maximum):
    i = 0
    while i < maximum:
        val = (yield i)
        # Якщо надали value, то змінити лічильник
        if val is not None:
            i = val
        else:
            i += 1

it = counter(10)

print(next(it))     # 0
print(next(it))     # 1

print(it.send(8))   # 8
print(next(it))     # 9

# Ще один виклик next(it) викличе помилку StopIteration.

Оскільки yield часто буде повертати None, ви повинні завжди перевіряти цей випадок. Не використовуйте його значення в виразах, якщо ви не впевнені, що метод send() буде єдиним методом, який використовується для поновлення вашої генераторної функції.

Крім методу send(), є ще два методи для генераторів:

  • throw(value) використовується для виклику винятку всередині генератора; виняток викликається виразом yield, де виконання генератора поставлене на паузу.

  • close() викликає виняток GeneratorExit всередині генератора для завершення ітерації. При отриманні цього винятку, код генератора повинен або викликати GeneratorExit, або StopIteration; перехоплення винятку і виконання чого-небудь іншого є незаконним і викличе RuntimeError. close() також буде викликано збирачем сміття Python, коли генератор буде зібрано.

Якщо вам потрібно виконати код очищення, коли настає GeneratorExit, я пропоную використовувати блок try: ... finally: замість перехоплення GeneratorExit.

Кумулятивний ефект цих змін полягає в перетворенні генераторів з односторонніх виробників інформації в виробників і споживачів.

Генератори також стають корутинами, більш узагальненою формою підпрограм. Підпрограми входять в один момент і виходять в інший момент (вершина функції і оператор return), але корутини можуть бути введені, виведені і відновлені в багатьох різних точках (оператори yield).

Текст на цій сторінці є перекладом "Functional Programming HOWTO", автор: A. M. Kuchling. Інформація про копірайт: History and License.