№30 Генераторы / для начинающих

Предыдущий урок: Приоритетность операторов

В этой статье вы научитесь создавать и использовать функции и выражения генераторов в Python. Также узнаете, зачем и когда их стоит использовать в программах. Будут рассмотрены основные отличия от итераторов и обычных функций.

Отдельное внимание будет уделено инструкции yield. Она является частью генератора и заменяет ключевое слово return. Когда программа доходит до yield, то функция переходит в состояние ожидания и продолжает работу с того же места при повторном вызове.

Генератор в Python — это функция с уникальными возможностями. Она позволяет приостановить или продолжить работу. Генератор возвращает итератор, по которому можно проходить пошагово, получая доступ к одному значению с каждой итерацией.

Генератор предоставляет способ создания итераторов, решая следующую распространенную проблему.

Создание итератора в Python — достаточно громоздкая операция. Для этого нужно написать класс и реализовать методы __iter__() и __next__(). После этого требуется настроить внутренние состояния и вызывать исключение StopIteration, когда больше нечего возвращать.

Как создать генератор в Python?

Генератор — это альтернативный и более простой способ возвращать итераторы. Процедура создания не отличается от объявления обычной функции.

Есть два простых способа создания генераторов в Python.

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

Генератор создается по принципу обычной функции.

Отличие заключается в том, что вместо return используется инструкция yield. Она уведомляет интерпретатор Python о том, что это генератор, и возвращает итератор.

Синтаксис функции генератора:


def gen_func(args):
...
while [cond]:
...
yield [value]

Return всегда является последней инструкцией при вызове функции, в то время как yield временно приостанавливает исполнение, сохраняет состояние и затем может продолжить работу позже.

Дальше простейший пример функции генератора Python, которая определяет следующее значение в последовательности Фибоначчи.

Демонстрация функции генератора Python:


def fibonacci(xterms):
# первые два условия
x1 = 0
x2 = 1
count = 0

if xterms <= 0: print("Укажите целое число больше 0") elif xterms == 1: print("Последовательность Фибоначчи до", xterms, ":") print(x1) else: while count < xterms: xth = x1 + x2 x1 = x2 x2 = xth count += 1 yield xth
fib = fibonacci(5)

print(next(fib))
print(next(fib))
print(next(fib))
print(next(fib))
print(next(fib))
print(next(fib))

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

После создания функции генератора вызываем ее, передав 5 в качестве аргумента. Она вернет только объект итератора.

Такая функция не будет выполняться до тех пор, пока не будет вызван метод next() с вернувшимся объектом в качестве аргумента (то есть fib). Для итерации повторим все шаги шесть раз.

Первые пять вызовов next() были успешными и возвращали соответствующий элемент последовательности Фибоначчи. А вот последний вернул исключение StopIteration, поскольку элементов, которые можно было бы вернуть, больше не осталось.

Вот что будет выведено после выполнения.

1
2
3
5
8
Traceback (most recent call last):
File «C:/Python/Python3/python_generator.py», line 29, in
print(next(fib))
StopIteration

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

Python позволяет писать выражения генератора для создания анонимных функций генератора. Процесс напоминает создание лямбда-функций для создания анонимных функций.

Синтаксис похож на используемый для создания списков с помощью цикла for. Однако там применяются квадратные скобки, а здесь — круглые.


# Синтаксис выражения генератора

gen_expr = (var**(1/2) for var in seq)

Еще одно отличие между «list comprehension» и «выражением генератора» в том, что при создании списков возвращается целый список, а в случае с генераторами — только одно значение за раз.

Пример выражение генератора Python:


# Создаем список
alist = [4, 16, 64, 256]

# Вычислим квадратный корень, используя генерацию списка
out = [a**(1/2) for a in alist]
print(out)

# Используем выражение генератора, чтобы вычислить квадратный корень
out = (a**(1/2) for a in alist)
print(out)
print(next(out))
print(next(out))
print(next(out))
print(next(out))
print(next(out))

В примере выше out вернет список со значениями, возведенными в квадрат. То есть, сразу готовый результат.

Выражение генератора вернет итератор, который будет выдавать по одному значению за раз. В списке 4 элемента. Таким образом четыре последовательных вызова метода next() напечатают квадратные корни соответствующих элементов списка.

Но поскольку метод был вызван 5 раз, то вернулось также исключение StopIteration.

[2.00, 4.0, 8.00, 16.0]
at 0x000000000359E308>
2.0
4.0
8.0
16.0
Traceback (most recent call last):
File «C:/Python/Python3/python_generator.py», line 17, in
print(next(out))
StopIteration

Как использовать генератор в Python?

Теперь пришло время разобраться с тем, как использовать генератор в программах. В прошлых примерах метод next() применялся по отношению к итератору, который возвращала функция генератора.

С помощью метода next()

Метод next() — самый распространенный способ для получения значения из функции генератора. Вызов метода приводит к выполнению, что возвращает результат тому, кто делал вызов.


В примере ниже значения выводятся с помощью генератора.

Демонстрация генератора next():


alist = ['Python', 'Java', 'C', 'C++', 'CSharp']

def list_items():
for item in alist:
yield item

gen = list_items()

iter = 0

while iter < len(alist):
print(next(gen))
iter += 1

Этот пример не отличается от предыдущих, но каждый элемент здесь возвращается генератором с помощью метода next(). Для этого сперва создается объект генератора gen, который является идентификатором, хранящим состояние генератора.

Каждый вызов next() объекта генератора приводит к выполнению вплоть до инструкции yield. Затем Python возвращает значение и сохраняет состояние для последующего использования.

Использование цикла for

Также можно использовать цикл for для итерации по объекту генератора. В этом случае вызов next() происходит неявно, но элементы все равно возвращаются один за одним.

Генератор для демонстрации цикла:

alist = ['Python', 'Java', 'C', 'C++', 'CSharp']

def list_items():
for item in alist:
yield item

gen = list_items()

for item in gen:
print(item)

Return vs. yield

Ключевое слово return — это финальная инструкция в функции. Она предоставляет способ для возвращения значения. При возвращении весь локальный стек очищается. И новый вызов начнется с первой инструкции.

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

Генератор vs. функция

Дальше перечислены основные отличия между генератором и обычной функцией.

  • Генератор использует yield для отправления значения пользователю, а у функции для этого есть return;
  • При использовании генератора вызовов yield может быть больше чем один;
  • Вызов yield останавливает исполнение и возвращает итератор, а return всегда выполняется последним;
  • Вызов метода next() приводит к выполнению функции генератора;
  • Локальные переменные и состояния сохраняются между последовательными вызовами метода next();
  • Каждый дополнительный вызов next() вызывает исключение StopIteration, если нет следующих элементов для обработки.

Дальше пример функции генератора с несколькими yield.



def testGen():
x = 2
print('Первый yield')
yield x

x *= 1
print('Второй yield')
yield x

x *= 1
print('Последний yield')
yield x

# Вызов генератора
iter = testGen()

# Вызов первого yield
next(iter)

# Вызов второго yield
next(iter)

# Вызов последнего yield
next(iter)

Вывод будет такой.

Первый yield
Второй yield
Последний yield

Когда использовать генератор?

Есть много ситуаций, когда генератор оказывается полезным. Вот некоторые из них:

  • Генераторы помогают обрабатывать большие объемы данных. Они позволяют производить так называемые ленивые вычисления. Подобным образом происходит потоковая обработка.
  • Генераторы можно устанавливать друг за другом и использовать их как Unix-каналы.
  • Генераторы позволяют настроить одновременное исполнение
  • Они часто используются для чтения крупных файлов. Это делает код чище и компактнее, разделяя процесс на более мелкие сущности.
  • Генераторы особенно полезны для веб-скрапинга и увеличения эффективности поиска. Они позволяют получить одну страницу, выполнить какую-то операцию и двигаться к следующей. Этот подход куда эффективнее чем получение всех страниц сразу и использование отдельного цикла для их обработки.

Зачем использовать генераторы?

Генераторы предоставляют разные преимущества для программистов и расширяют особенности, которые проявляются во время выполнения.

Удобные для программистов

Генератор кажется сложной концепцией, но его легко использовать в программах. Это хорошая альтернатива итераторам.


Рассмотрим следующий пример реализации арифметической прогрессии с помощью класса итератора.

Создание арифметической прогрессии с помощью класса итератора:

class AP:
def __init__(self, a1, d, size):
self.ele = a1
self.diff = d
self.len = size
self.count = 0

def __iter__(self):
return self

def __next__(self):
if self.count >= self.len:
raise StopIteration
elif self.count == 0:
self.count += 1
return self.ele
else:
self.count += 1
self.ele += self.diff
return self.ele

for ele in AP(1, 2, 10):
print(ele)

Ту же логику куда проще написать с помощью генератора.

Генерация арифметической прогрессии с помощью функции генератора:

def AP(a1, d, size):
count = 1
while count <= size: yield a1 a1 += d count += 1
for ele in AP(1, 2, 10):
print(ele)

Экономия памяти

Есть использовать обычную функцию для возвращения списка, то она сформирует целую последовательность в памяти перед отправлением. Это приведет к использованию большого количества памяти, что неэффективно.

Генератор же использует намного меньше памяти за счет обработки одного элемента за раз.

Обработка больших данных

Генераторы полезны при обработке особенно больших объемов данных, например, Big Data. Они работают как бесконечный поток данных.

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

Следующий код теоретически может выдать все простые числа.

Стоит отметить, что он запустит бесконечный цикл, для остановки которого нужно нажать Ctrl + C.

Найдем все простые числа с помощью генератора:


def find_prime():
num = 1
while True:
if num > 1:
for i in range(2, num):
if (num % i) == 0:
break
else:
yield num
num += 1

for ele in find_prime():
print(ele)

Последовательность генераторов

С помощью генераторов можно создать последовательность разных операций. Это более чистый способ разделения обязанностей между всеми компонентами и последующей интеграции их для получения нужного результата.

Цепочка нескольких операций с использованием pipeline генератора:


def find_prime():
num = 1
while num < 100: if num > 1:
for i in range(2, num):
if (num % i) == 0:
break
else:
yield num
num += 1

def find_odd_prime(seq):
for num in seq:
if (num % 2) != 0:
yield num

a_pipeline = find_odd_prime(find_prime())

for a_ele in a_pipeline:
print(a_ele)

В примере ниже связаны две функции. Первая находит все простые числа от 1 до 100, а вторая — выбирает нечетные.

Выводы

Генераторы создают последовательность на лету, что позволяет получать доступ к одному элементу в любой момент. Это не требует большого количества памяти и оставляет возможность работать с бесконечными потоками данных. Это довольна сложная концепция, которую все равно стоит попробовать внедрить в реальные проекты.

Далее: filter() в python