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

Что такое итерируемый объект?

подробнее

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

  • __iter__(), который отдаёт итератор.
  • __getitem__(), позволяет получать элементы по индексу

Примеры итерируемых объектов: списки, кортежи, строки, словари, множества

Примечание:

Если объект реализует метод __getitem__() с целочисленными индексами, но не реализует __iter__(), он все равно считается итерируемым. В этом случае Python автоматически создает итератор, который использует __getitem__() для последовательного получения элементов, начиная с индекса 0, пока не возникнет исключение IndexError.

Что такое итератор?

подробнее

Итератор - это объект, который реализует протокол итератора (методы: __iter__() и __next__())

  • __iter__() — возвращает сам итератор (сам объект self)
  • __next__() — возвращает следующий элемент или возбуждает исключение StopIteration когда элементы закончились.

Итератор запоминает свою текущую позицию в последовательности. После того как мы прошли по всем элементам один раз, итератор “исчерпан” и больше не может возвращать элементы. Всегда можно снова получить итератор и снова пробежаться.

Из коллекции (итерируемого объекта) можно получить итератор вызвав на ней метод iter(). Пример: iter([1,2,3])

Пример итератора:

class CountDown:
    """Итератор обратного отсчета от заданного числа до 0"""
    
    def __init__(self, start):
        self.start = start
    
    def __iter__(self):
        return self  # Возвращаем сам итератор
    
    def __next__(self):
        if self.start < 0:
            raise StopIteration  # Заканчиваем итерацию
        else:
            current = self.start
            self.start -= 1
            return current

# Использование:
countdown = CountDown(3)
for num in countdown:
    print(num)
# Вывод: 3, 2, 1, 0

# Повторное использование:
print(list(countdown))  # [] - пустой список, так как итератор исчерпан

Что такое генератор?

подробнее

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

Пример генератора:

def my_generator():
    yield 1
    yield 2
    yield 3

gen = my_generator()
print(next(gen))  # 1
print(next(gen))  # 2
print(next(gen))  # 3
print(next(gen))  # StopIteration - исключение, так как элементы закончились
# Генератор чисел Фибоначчи
def fibonacci(n):
    a, b = 0, 1
    count = 0
    while count < n:
        yield a
        a, b = b, a + b
        count += 1

Что такое генераторное выражение?

подробнее

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

Пример:

# Генераторное выражение
gen_expr = (x**2 for x in range(5))
print(gen_expr)  # <generator object <genexpr> at 0x...>
print(list(gen_expr))  # [0, 1, 4, 9, 16]

Как работает yield?

подробнее

yield - переводится как “уступать”.
Это оператор, который:

  • Возвращает значение из функции-генератора
  • Приостанавливает выполнение функции, сохраняя её состояние
  • Уступает выполнение вызывающему коду
  • При следующем вызове продолжает выполнение с места останова

Зачем нужна конструкция yield from?

подробнее

yield from используется если в одном генераторе нужно вернуть все значения из другого.

Пример:

def first_gen():
    yield 1
    yield 11

def second_gen():
    yield from first_gen()
    yield 2
    yield 22

sg = second_gen()
for val in sg:
    print(val)  # 1, 11, 2, 22

Отличие итератора от генератора

подробнее

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

Ключевые отличия:

  • Генератор — это подмножество итераторов (все генераторы — итераторы, но не наоборот)
  • Генераторы по умолчанию ведут себя лениво, итераторы тоже так могут, но зависит от реализации.
  • У генераторов более лаконичный код:
    • Генераторы автоматически реализуют протокол итератора, а в итераторах надо реализовывать вручную.
    • Генераторы автоматически сохраняют состояние между вызовами next(), а в итераторах надо описывать в __next__().
      (в генераторах сохранение между вызовами делает сам Python: после yield выполнение приостанавливается, а все локальные переменные остаются в памяти до следующего вызова next())
      (в ручном итераторе же мы должны сами придумать, где и как это состояние хранить (атрибуты объекта) и как возобновлять выполнение — Python за нас это не сделает)

Протоколы __iter__ и __next__

подробнее

Чтобы объект был итерируемым он должен реализовывать метод __iter__().
Чтобы объект был итератором он должен реализовывать методы: __iter__() и __next__().

  • __iter__(self)
    • Делает объект «перебираемым» (итерируемым).
    • Возвращает: Объект-итератор. (Итератор вернёт self, итерируемый объект вернёт новый итератор)
    • Вызывается: В самом начале цикла for или при вызове iter(obj).
  • __next__(self)
    • Возвращает: Следующий элемент коллекции.
    • Когда элементы кончились, метод обязан выбросить исключение StopIteration.
    • Вызывается: На каждом шаге цикла или при вызове next(obj).