• en
  • Language: ru
  • Documentation version: latest

3. Генераторы

Сначала разберемся, что такое итераторы. Согласно Википедии, итератор - это объект, который позволяет программисту обходить контейнер, в частности списки. Однако итератор выполняет обход и предоставляет доступ к элементам данных в контейнере, но не выполняет итерацию. Возможно, вы запутались, поэтому давайте разберемся с этим немного медленнее. Есть три части, а именно:

  • Iterable

  • Итератор

  • Итерация

Все эти части связаны друг с другом. Мы обсудим их по очереди и позже поговорим о генераторах.

3.1. Iterable

iterable - это любой объект в Python, у которого определен метод __iter__ или __getitem__, который возвращает итератор или может принимать индексы (Подробнее о них можно прочитать here). Короче говоря, iterable - это любой объект, который может предоставить нам итератор. Так что же такое итератор?

3.2. Итератор

Итератор - это любой объект в Python, для которого определен метод next (Python2) или __next__. Вот и все. Это итератор. Теперь давайте разберемся, что такое итерация.

3.3. Итерация

Проще говоря, это процесс извлечения элемента из чего-то, например, из списка. Когда мы используем цикл для обхода чего-либо, это называется итерацией. Это название, данное самому процессу. Теперь, когда у нас есть базовое понимание этих терминов, давайте разберемся в генераторах.

3.4. Генераторы

Генераторы - это итераторы, но итерацию над ними можно выполнить только один раз. Это потому, что они не хранят все значения в памяти, а генерируют их на лету. Вы используете их путем итерации, либо с помощью цикла „for“, либо передавая их в любую функцию или конструкцию, которая выполняет итерацию. В большинстве случаев generators реализуются как функции. Однако они не return значение, а yield его. Вот простой пример функции generator:

def generator_function():
    for i in range(10):
        yield i

for item in generator_function():
    print(item)

# Output: 0
# 1
# 2
# 3
# 4
# 5
# 6
# 7
# 8
# 9

В этом случае он не очень полезен. Генераторы лучше всего подходят для вычисления больших наборов результатов (особенно для вычислений с использованием циклов), когда вы не хотите выделять память для всех результатов одновременно. Многие функции Стандартной библиотеки, возвращающие lists в Python 2, были изменены на generators в Python 3, поскольку generators требуют меньше ресурсов.

Вот пример generator, который вычисляет числа Фибоначчи:

# generator version
def fibon(n):
    a = b = 1
    for i in range(n):
        yield a
        a, b = b, a + b

Теперь мы можем использовать его следующим образом:

for x in fibon(1000000):
    print(x)

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

def fibon(n):
    a = b = 1
    result = []
    for i in range(n):
        result.append(a)
        a, b = b, a + b
    return result

Это потребовало бы всех наших ресурсов при вычислении большого входного значения. Мы обсуждали, что итерацию над generators можно выполнить только один раз, но не проверяли это. Перед тестированием необходимо узнать еще об одной встроенной функции Python, next(). Она позволяет нам получить доступ к следующему элементу последовательности. Итак, давайте проверим наше понимание:

def generator_function():
    for i in range(3):
        yield i

gen = generator_function()
print(next(gen))
# Output: 0
print(next(gen))
# Output: 1
print(next(gen))
# Output: 2
print(next(gen))
# Output: Traceback (most recent call last):
#            File "<stdin>", line 1, in <module>
#         StopIteration

Как мы видим, после выхода всех значений next() возникла ошибка StopIteration. В основном эта ошибка сообщает нам, что все значения были выданы. Вы можете задаться вопросом, почему мы не получаем эту ошибку при использовании цикла for? Ответ прост. Цикл for автоматически перехватывает эту ошибку и прекращает вызов next. Знаете ли вы, что несколько встроенных типов данных в Python также поддерживают итерацию? Давайте проверим это:

my_string = "Yasoob"
next(my_string)
# Output: Traceback (most recent call last):
#      File "<stdin>", line 1, in <module>
#    TypeError: str object is not an iterator

Это не то, чего мы ожидали. Ошибка говорит, что str не является итератором. И это правильно! Это итерабельность, но не итератор. Это означает, что он поддерживает итерацию, но мы не можем итерировать его напрямую. Так как же нам его итерировать? Пришло время познакомиться с еще одной встроенной функцией iter. Она возвращает объект iterator из итерабельной таблицы. Хотя int не является итерабельной, мы можем использовать ее на строке!

int_var = 1779
iter(int_var)
# Output: Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# TypeError: 'int' object is not iterable
# This is because int is not iterable

my_string = "Yasoob"
my_iter = iter(my_string)
print(next(my_iter))
# Output: 'Y'

Теперь это гораздо лучше. Я уверен, что вам понравилось изучать генераторы. Имейте в виду, что вы сможете полностью усвоить эту концепцию только тогда, когда будете ее использовать. Убедитесь, что вы следуете этому образцу и используете generators всякий раз, когда они имеют для вас смысл. Вы не будете разочарованы!