Генераторы в Python - это больше, чем просто альтернатива спискам

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

Да, я знаю, что это ничего не будет значить для вас без примера, поэтому давайте сразу перейдем к этому с помощью приведенных ниже фрагментов кода:

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

Теперь давайте запустим эту функцию так, как должен запускаться генератор:

Сначала мы инициализируем наш объект-генератор с помощью temp_gen1 = gen1(). Далее мы используем ключевое слово next. Это позволяет нам перейти к следующей итерации нашего оператора yield. Поскольку ключевое слово next вызывается впервые, оно должно перейти в наш “I am the bone of my sword”.

Давай проверим!

Выход:

I am the bone of my sword

Хорошо, он вернул первый оператор yield. Не очень интересно. Добавим еще next(temp_gen1) и напечатаем в консоли:

print(  next(temp_gen1) )

Выход:

I am the bone of my sword
Steel is my body and fire is my blood

Теперь посмотрим, что происходит? Он сразу перешел к следующему оператору yield!

Добавим еще next(temp_gen1) и распечатаем его в консоли:

print(  next(temp_gen1) )

Выход:

I am the bone of my sword
Steel is my body and fire is my blood
I have created over a thousand blades

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

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

Полный код:

Вам может быть любопытно, что было бы, если бы не было следующего yield звонка, верно? Ну, это просто выбросит StopIteration исключение.

Давайте попробуем, напечатав еще next(temp_gen1) и выполнив.

Выход:

I am the bone of my sword
Steel is my body and fire is my blood
I have created over a thousand blades
Traceback (most recent call last):
  File "d:/Jordan_Williams/coding1/Coding/Medium/idea 9/main.py", line 16, in <module>
    print(  next(temp_gen1) )
StopIteration

Выброшен StopIteration. Чтобы начать снова, необходимо переназначить генератор.

Сохранение состояния

Генераторы не просто выполняют следующий yield каждого вызова. Они также поддерживают состояние.

Давайте посмотрим на пример ниже, используя ту же функцию:

В приведенном выше фрагменте кода мы объявили генератор, который выдает первые три четных числа. В генераторе переменная even каждый раз увеличивается на 2, а затем передается в yield, чтобы каждый раз возвращать ее.

Давайте распечатаем их и посмотрим, что они получат:

Выход:

initialize counter
First even:
2
Second even:
4
Third even:
6

Как видите, функция смогла вывести первые три четных числа. Переменная even сохранялась на протяжении всей функции.

Полный код:

Генераторы зацикливания

Генераторы также можно перебирать, как обычный список.

Вместо печати, как в предыдущем примере, мы будем использовать цикл:

Выход:

initialize counter
First even:
2
Second even:
4
Third even:
6

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

Эффективность памяти

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

Следовательно, если бы мы инициализировали это с последовательностью диапазона 100-100000000, все числа были бы сгенерированы.

data = seq(100,100_000_000)

Пришло время казни этого поколения.

Для этого нам нужно импортировать библиотеку времени Python:

import time

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

data = seq(100,100_000_000) рассчитывается от начала до конца с использованием time.time(). Эта функция возвращает текущее время в секундах.

Выход:

execution time: 8.553153991699219

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

Полный код:

В отличие от фрагмента кода выше, генераторы создают последовательность по запросу. Элемент загружается только при использовании ключевого слова next.

Давайте посмотрим на функцию, эквивалентную той, что использовалась ранее, но на этот раз с использованием синтаксиса генератора:

Давайте заменим эту функцию в предыдущем примере и запустим ее.

Выход:

execution time: 0.0

0,0 секунды. Несмотря на то, что это время может быть неточным на 100%, вы замечаете мощность генераторов. Никакой элемент не создается, пока не будет использовано ключевое слово next или цикл.

Преимущества памяти

  1. Память не бесконечна. Обычный список будет хранить все в памяти и может привести к сбою вашей системы из-за больших потоков данных.
  2. Возможно, вам не нужны все данные сразу или даже все данные в первую очередь.

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

В Python часто используются функции map, filter и open. В Python 2 использование этих функций вернет список. Однако, начиная с Python 3, их реализация переключилась на генераторы.

Карта

Фильтр

Открыть

Во всех этих примерах ключевое слово next используется для перехода к следующей последовательности.

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

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

Внимательно посмотрите на эту функцию: while True. Цикл никогда не закончится, так как он дает квадрат каждого числа от 1 до бесконечности. Однако из-за генераторов это не проблема.

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

Выход:

1
4
9

Видите, он производит квадрат чисел 1, 2 и 3. Это можно делать бесконечное количество раз.

Полный код:

Генератор выражения

>>> list_compr = [i ** 2 for i in range(5)]
>>> list_compr
[0, 1, 4, 9, 16]
>>>

Приведенный выше фрагмент иллюстрирует понимание списка. Это питонический способ создания списка.

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

>>> list_generator = (i ** 2 for i in range(5))
>>> list_generator
<generator object <genexpr> at 0x000001F7F60D3CC8>
>>>

Посмотрите внимательно.

Для понимания списка используются квадратные скобки ([i ** 2 for i in``` range(5)]), а в генераторах используются круглые скобки ((i ** 2 for i in range(5))). Эта небольшая разница превращается в генератор.

Удобно, правда?

Давайте проверим это:

>>> next(list_generator)
0
>>> next(list_generator)
1
>>> next(list_generator)
4
>>> next(list_generator)
9
>>> next(list_generator)
16
>>>

Заключение

Генераторы могут быть полезной заменой спискам. Их можно использовать, когда есть большие объемы данных или если вам нужно удобство предоставления следующего элемента в потоке. На их основе построено множество библиотек Python. Я определенно рекомендую вам попробовать их в собственном коде.

Давайте продолжим вместе расти в нашем путешествии по Python!