Итераторы и генераторы в Python

Итераторы – это объекты, которые можно проитерировать (перебрать в цикле). Это одна из фич языка Python, аккуратно припрятанная для циклов и представлений списков (list comprehensions). Любой объект, из которого можно получить итератор, называется итерируемым объектом (англ. iterable).

Для создания итератора необходимо проделать большую работу. Например, реализация любого объекта итератора должна содержать методы __iter__() и __next__(). Также она должна иметь способ отслеживать внутреннее состояние объекта и вызывать исключение StopIteration, когда больше не может быть возвращено ни одного значения. Эти правила известны как протокол итератора.

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

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

Глоссарий

ТерминОпределение
Итерируемый объект (iterable)Объект Python, который можно перебрать в цикле. Примерами служат списки, множества, кортежи, словари, строки и т.д.
Итератор (iterator)Итератор – это объект, который можно итерировать. Итераторы содержат исчислимое количество значений.
Генератор (generator)Особый тип функции, который возвращает не единичное значение, а объект итератора с последовательностью значений.
Ленивые вычисления (lazy evaluation)Стратегия вычислений, при которой вычисления следует откладывать до тех пор, пока не понадобится их результат.
Протокол итератораНабор правил, которым необходимо следовать, чтобы определить итератор в Python.
next()Встроенная функция, используемая для возврата следующего элемента в итераторе.
iter()Встроенная функция, используемая для преобразования итерируемого объекта в итератор.
yield()Ключевое слово Python, аналогичное ключевому слову return, за исключением того, что yield возвращает не значение, а объект генератора.

Итераторы и итерируемые объекты в Python

Итерируемые объекты – это объекты, способные возвращать свои члены по одному за раз. Как правило, итерируемым считается любой объект, который можно перебрать в цикле for. Популярные встроенные структуры данных Python, такие как списки, кортежи и множества, считаются итерируемыми объектами.

Изучение итерируемых объектов в Python на примерах

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

Чтобы продемонстрировать эту функциональность, мы создадим список, который является итерируемым объектом, а затем создадим итератор, передав этот список встроенной функции iter().

list_instance = [1, 2, 3, 4]
print(iter(list_instance))

"""
<list_iterator object at 0x7fd946309e90>
"""

Хотя сам по себе список не является итератором, вызов функции iter() преобразует его таковой и возвращает объект итератора.

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

list_instance = [1, 2, 3, 4]
print(next(list_instance))
"""
--------------------------------------------------------------------
TypeError                         Traceback (most recent call last)
<ipython-input-2-0cb076ed2d65> in <module>()
    3 print(iter(list_instance))
    4
----> 5 print(next(list_instance))
TypeError: 'list' object is not an iterator
"""

Как видите, попытка вызвать функцию next() для списка вызвала ошибку TypeError. Это произошло потому, что список является итерируемым объектом, но не итератором.

Изучение итераторов в Python на примерах

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

# Создание объекта списка
list_instance = [1, 2, 3, 4]

# Преобразование списка в итератор
iterator = iter(list_instance)

# Возврат элементов по одному за раз
print(next(iterator))
print(next(iterator))
print(next(iterator))
print(next(iterator))
"""
1
2
3
4
"""

Python автоматически создает объект-итератор, когда вы пытаетесь перебрать итерируемый объект в цикле.

# Создание объекта списка
list_instance = [1, 2, 3, 4]

# Перебор списка в цикле
for iterator in list_instance:
  print(iterator)
"""
1
2
3
4
"""

Когда будет поймано исключение StopIteration, цикл завершится.

Значения, получаемые из итератора, могут быть получены только слева направо. В Python нет функции previous() (англ. previous переводится как «предыдущий»), которая позволяла бы двигаться по итератору в обратном направлении.

Ленивая природа итераторов

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

list_instance = [1, 2, 3, 4]
iterator_a = iter(list_instance)
iterator_b = iter(list_instance)
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"B: {next(iterator_b)}")
"""
A: 1
A: 2
A: 3
A: 4
B: 1
"""

Обратите внимание, что iterator_b печатает первый элемент серии.

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

Однако из итератора можно извлечь и все значения сразу, вызвав контейнер встроенной итерируемой структуры данных Python (например, list(), set(), tuple()) на объекте итератора.

# Создание итерируемого объекта
list_instance = [1, 2, 3, 4]

# Создание итератора из итерируемого объекта
iterator = iter(list_instance)
print(list(iterator))
"""
[1, 2, 3, 4]
"""

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

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

Генераторы Python

Наиболее целесообразной альтернативой реализации итератора является использование генератора. Хотя генераторы могут выглядеть как обычные функции Python, они отличаются от них. Для начала, объект-генератор не возвращает элементы. Вместо этого он использует ключевое слово yield для генерации элементов на лету. Таким образом, можно сказать, что генератор – это особый вид функции, использующий ленивые вычисления.

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

def factors(n):
  factor_list = []
  for val in range(1, n+1):
      if n % val == 0:
          factor_list.append(val)
  return factor_list

print(factors(20))
"""
[1, 2, 4, 5, 10, 20]
"""

Этот код возвращает весь список множителей.

А теперь давайте используем генератор:

def factors(n):
  for val in range(1, n+1):
      if n % val == 0:
          yield val
print(factors(20))

"""
<generator object factors at 0x7fd938271350>
"""

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

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

def factors(n):
  for val in range(1, n+1):
      if n % val == 0:
          yield val
         
factors_of_20 = factors(20)
print(next(factors_of_20))

"""
1
"""

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

print((val for val in range(1, 20+1) if n % val == 0))
"""
<generator object <genexpr> at 0x7fd940c31e50>
"""

Ключевое слово yield в Python

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

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

Функция продолжит работу с момента вызова yield. Например:

def yield_multiple_statments():
  yield "This is the first statment"
  yield "This is the second statement"  
  yield "This is the third statement"
  yield "This is the last statement. Don't call next again!"
example = yield_multiple_statments()
print(next(example))
print(next(example))
print(next(example))
print(next(example))
print(next(example))
"""
This is the first statment
This is the second statement
This is the third statement
This is the last statement. Don't call next again or else!
--------------------------------------------------------------------
StopIteration                  Traceback (most recent call last)
<ipython-input-25-4aaf9c871f91> in <module>()
    11 print(next(example))
    12 print(next(example))
---> 13 print(next(example))
StopIteration:
"""

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

Подведение итогов

Напомним, что итераторы – это объекты, по которым можно выполнять итерации, а генераторы – это специальные функции, использующие ленивые вычисления. Реализация собственного итератора означает, что вы должны создать методы __iter__() и __next__(), а генератор можно реализовать с помощью ключевого слова yield в функции.

Вы можете предпочесть использовать собственный итератор, а не генератор, если вам нужен объект со сложным поведением, поддерживающим состояние, или если вы хотите раскрыть другие методы, помимо __next__(), __iter__() и __init__(). С другой стороны, генератор может быть предпочтительнее при работе с большими наборами данных, поскольку он не хранит их содержимое в памяти, или когда просто нет необходимости реализовывать итератор.

Перевод статьи «Python Iterators and Generators Tutorial».

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *