В чём разница между итераторами и генераторами в Python?

Ответ

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

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

Пример итератора (класс):

class CountDown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current <= 0:
            raise StopIteration
        num = self.current
        self.current -= 1
        return num

for num in CountDown(3):
    print(num)  # 3, 2, 1

Пример генератора (функция с yield):

def count_down(start):
    current = start
    while current > 0:
        yield current
        current -= 1

for num in count_down(3):
    print(num)  # 3, 2, 1

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

  1. Синтаксис: Генераторы проще и короче писать.
  2. Память: Генераторы не хранят всю последовательность в памяти, что критично для больших или бесконечных потоков данных.
  3. Состояние: Генератор автоматически сохраняет состояние выполнения между вызовами __next__(). После истощения (StopIteration) его нельзя переиспользовать.
  4. Использование: Любой генератор является итератором, но не наоборот.

Ответ 18+ 🔞

А, ну ты про эту классику, про итераторы с генераторами! Давай разжую, как есть, без этих ваших заумных терминов, от которых мозг вскипает, как каша в микроволновке.

Смотри, представь себе, что тебе нужно перебрать кучу каких-то штук. Например, мешок картошки. Итератор — это как будто ты сам лезешь в этот мешок рукой и вытаскиваешь по одной картофелине. Ты знаешь, как это делать: засунул руку, нащупал, вытащил, положил в ведро. Повторяешь, пока мешок не опустеет. Вот этот твой навык «лезть и вытаскивать» — это и есть протокол итератора. Ты можешь даже написать инструкцию (класс), как это делать правильно: «1. Проверить, есть ли ещё картошка. 2. Если нет — крикнуть „Всё!“. 3. Если есть — схватить одну. 4. Отдать её». Всё чинно, благородно, но мороки — овердохуища.

А теперь генератор — это волшебный мешок-непереливайка. Ты просто говоришь ему: «Ну-ка, дай картошку». И он тебе выплёвывает одну. Говоришь снова — он выплёвывает следующую. И так до тех пор, пока не кончится. А внутри него, в этом мешке, какая-то хитрая жопа с моторчиком, которая сама знает, какую картошку тебе выдать следующей. И самое главное — он не вываливает все пятьдесят килограмм тебе на голову сразу, экономя место в твоей кухне (то бишь, оперативке).

Вот смотри на код, тут всё наглядно.

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

class CountDown:
    def __init__(self, start):
        self.current = start  # Запоминаем, с какой цифры начинаем

    def __iter__(self):
        return self  # Говорим: "Итератор — это я сам, красавчик"

    def __next__(self):
        if self.current <= 0:  # Если дошло до нуля
            raise StopIteration  # Всё, приехали, конец программы
        num = self.current  # Берём текущее число
        self.current -= 1   # Готовим следующее (уменьшаем на 1)
        return num          # Возвращаем то, что взяли

# Используем как обычную штуку для цикла
for num in CountDown(3):
    print(num)  # Напечатает 3, потом 2, потом 1

Видишь, сколько телодвижений? Целый класс, методы эти __что-то-там__... Прям терпения ноль, ебать.

А теперь генератор, та же логика, но в разы проще:

def count_down(start):
    current = start
    while current > 0:  # Пока текущее число больше нуля
        yield current   # Выплёвываем его наружу и замираем
        current -= 1    # Уменьшаем. Этот шаг выполнится, когда нас попросят "ну дай следующее!"

# Используем точно так же!
for num in count_down(3):
    print(num)  # И тоже напечатает 3, 2, 1

Ёпта! Вот это уже другое дело. Никаких классов, один ключевой момент — волшебное слово yield. Оно как бы говорит: «На, держи значение, но я тут присяду отдохну, а ты иди работай. Как будешь готов к следующему — позови, я продолжу с того же места». И функция превращается в эту самую ленивую, умную фабрику по производству значений.

Так в чём же, блядь, разница, если на выходе одно и то же?

  1. Писать проще. Генератор — это просто функция. Не надо городить класс с кучей методов. Это раз.
  2. Память не жрёт. Это самое важное. Представь, тебе нужно не 3 числа, а, скажем, все числа от 1 до 10 миллионов. Итератор-класс, если он тупо создаст список, попытается запихнуть все 10 миллионов чисел в память сразу — ядрёна вошь, комп захлебнётся. А генератор будет тихо так, по одному числу, тебе их отдавать, держа в памяти только одно текущее. Экономия — пиздец.
  3. Состояние само хранится. В генераторе переменная current сама запоминает своё значение между вызовами. В итераторе-классе ты за этим всем сам следишь, как собака за тапками.
  4. Одноразовость. И генератор, и такой простой итератор, как в примере, — одноразовые штуки. Выдохся (StopIteration) — и накрылся медным тазом. Переиспользовать нельзя, нужно новый создавать.

Короче, итог: Генератор — это ленивый итератор для бедных (в хорошем смысле). Проще, экономнее, часто удобнее. Любой генератор — уже готовый итератор. Но если тебе нужна какая-то нестандартная, пиздопроебибная логика перебора, которую в yield не впихнуть — тогда да, пишешь свой класс-итератор. Во всех остальных случаях — генераторы рулят, пизда рулю.