Генератори
Генератори (англ. 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.