Функции-генераторы
Источники:
- Саммерфилд, Глава 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.