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

Итераторы и Генераторы #

1. Что такое итератор и как он используется? #

Итератор в Python - это объект, который реализует методы iter и next .

Метод iter() возвращает сам итератор, а метод next возвращает следующий элемент последовательности. Когда элементы заканчиваются, next должен вызвать исключение StopIteration.

class Squares:
    def __init__(self, max_value):
        self.max_value = max_value
        self.current = 1

    def __iter__(self):
        # Метод __iter__ делает объект итератором. 
        # Возвращаем самого себя, так как класс сам является итератором
        return self

    def __next__(self):
        # Метод __next__ вызывается при каждой итерации
        if self.current <= self.max_value:
            result = self.current ** 2
            self.current += 1
            return result
        else:
            raise StopIteration

squares = Squares(5)
for square in squares:
    # На каждой итерации вызывается метод __next__ и выводится результат
    print(square)

Iterable (Итерабельный) — это объект, который можно итерировать (проходить в цикле for) - Должен реализовывать метод __iter__(), который возвращает итератор - Примеры: список, кортеж, строка, словарь - Можно итерировать многократно — каждый раз создается новый итератор

Iterator (Итератор) - Iterator — это объект, который непосредственно выполняет итерацию - Должен реализовывать метод __next__(), который возвращает следующий элемент - Должен реализовывать метод __iter__(), который возвращает сам себя - Проходится только один раз — после exhaustion (истощения) нельзя использовать снова

Ключевые различия

ХарактеристикаIterableIterator
Многократное использование✅ Да❌ Нет
СостояниеНе хранит состояние итерацииХранит текущее состояние
Методы__iter__()__iter__() и __next__()
Примерыlisttupledictiter(list)file object


2. Как реализовать протокол итератора и создать собственный итератор? #

Суть #

Чтобы объект был итератором, он должен реализовать два метода:

__iter__()
__next__()

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

Как работает for #

Цикл:

for item in obj:
    print(item)

примерно превращается в такую логику:

iterator = iter(obj)

while True:
    try:
        item = next(iterator)
    except StopIteration:
        break

    print(item)

Функция next() внутри вызывает у объекта метод __next__().

Минимальный собственный итератор #

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

    def __iter__(self):
        return self

    def __next__(self):
        if self.current <= 0:
            raise StopIteration

        value = self.current
        self.current -= 1
        return value

Использование:

counter = Countdown(3)

for number in counter:
    print(number)

Результат:

3
2
1

Что здесь происходит #

def __iter__(self):
    return self

Объект сам является своим итератором, поэтому возвращает self.

def __next__(self):

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

if self.current <= 0:
    raise StopIteration

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

value = self.current
self.current -= 1
return value

Метод возвращает текущее значение и сдвигает внутреннее состояние итератора.

Важный момент: итератор одноразовый #

counter = Countdown(3)

print(list(counter))  # [3, 2, 1]
print(list(counter))  # []

Почему второй список пустой? Потому что итератор уже дошёл до конца. После исчерпания итератор должен продолжать выбрасывать StopIteration.

Более правильный вариант: iterable + отдельный iterator #

Часто лучше разделять:

Iterable-объект
создаёт новый Iterator при каждом iter()
Iterator хранит состояние обхода

Пример:

class MyRange:
    def __init__(self, start, stop):
        self.start = start
        self.stop = stop

    def __iter__(self):
        return MyRangeIterator(self.start, self.stop)


class MyRangeIterator:
    def __init__(self, current, stop):
        self.current = current
        self.stop = stop

    def __iter__(self):
        return self

    def __next__(self):
        if self.current >= self.stop:
            raise StopIteration

        value = self.current
        self.current += 1
        return value

Использование:

numbers = MyRange(1, 4)

print(list(numbers))  # [1, 2, 3]
print(list(numbers))  # [1, 2, 3]

Здесь MyRangeитерируемый объект, но не сам итератор. Каждый вызов iter(numbers) создаёт новый независимый итератор.

Разница между iterable и iterator #

Iterable
объект, по которому можно пройтись в цикле for
должен иметь __iter__()

Iterator
объект, который реально выдаёт элементы по одному
должен иметь __iter__() и __next__()

Пример:

numbers = [1, 2, 3]

iterator = iter(numbers)

print(next(iterator))  # 1
print(next(iterator))  # 2
print(next(iterator))  # 3

Список — это iterable, но не iterator:

numbers = [1, 2, 3]

print(hasattr(numbers, "__iter__"))  # True
print(hasattr(numbers, "__next__"))  # False

А объект, возвращённый iter(numbers), уже iterator:

iterator = iter(numbers)

print(hasattr(iterator, "__iter__"))  # True
print(hasattr(iterator, "__next__"))  # True

Итог #

Минимальная реализация собственного итератора:

class MyIterator:
    def __iter__(self):
        return self

    def __next__(self):
        if нет_следующего_элемента:
            raise StopIteration

        return следующий_элемент

На практике чаще используют второй подход:

Контейнер / коллекция:
    __iter__() возвращает новый итератор

Итератор:
    __iter__() возвращает self
    __next__() возвращает следующий элемент

Так объект можно безопасно итерировать несколько раз.


3. Как реализовать генератор? (Не используя __iter__ и __next__) #

Генератор в Python реализуется через функцию с yield, без ручного написания __iter__() и __next__().

def my_generator():
    yield 1
    yield 2
    yield 3

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

Пример использования #

def numbers():
    yield 1
    yield 2
    yield 3


gen = numbers()

print(next(gen))  # 1
print(next(gen))  # 2
print(next(gen))  # 3

Или через for:

for number in numbers():
    print(number)

Вывод:

1
2
3

Генератор с циклом #

def countdown(n):
    while n > 0:
        yield n
        n -= 1


for value in countdown(5):
    print(value)

Вывод:

5
4
3
2
1

Что делает yield #

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

Пример:

def example():
    print("start")
    yield 10

    print("middle")
    yield 20

    print("end")
    yield 30


gen = example()

print(next(gen))
print(next(gen))
print(next(gen))

Вывод:

start
10
middle
20
end
30

Практический пример #

Генератор квадратов чисел:

def squares(limit):
    for number in range(limit):
        yield number ** 2


for square in squares(5):
    print(square)

Вывод:

0
1
4
9
16

Бесконечный генератор #

def counter(start=0):
    while True:
        yield start
        start += 1

Использование:

gen = counter()

print(next(gen))  # 0
print(next(gen))  # 1
print(next(gen))  # 2

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

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

Для простых случаев можно использовать генераторное выражение:

gen = (x ** 2 for x in range(5))

for value in gen:
    print(value)

Это похоже на list comprehension, но значения создаются лениво, по одному.

lst = [x ** 2 for x in range(5)]  # список сразу в памяти
gen = (x ** 2 for x in range(5))  # генератор

Главное отличие от класса-итератора #

Через класс:

class MyIterator:
    def __iter__(self):
        return self

    def __next__(self):
        ...

Через генератор:

def my_generator():
    yield ...

То есть yield — это более короткий и удобный способ создать итератор без ручной реализации __iter__() и __next__(). Python делает эту работу внутри генераторного объекта.


4. Какую роль выполняет метод __next__ в итерационном протоколе и какую логику он обязан реализовывать? #

Роль __next__ #

__next__() — это метод, который отвечает за выдачу следующего элемента итератора.

Когда ты вызываешь:

next(iterator)

Python внутри вызывает:

iterator.__next__()

То есть __next__() — это основная рабочая часть итератора.

Что обязан делать __next__ #

Метод __next__() обязан реализовывать три вещи:

1. Вернуть следующий элемент #

def __next__(self):
    return value

Например, если итератор проходит по числам от 0 до 3, то каждый вызов должен возвращать следующее число:

0
1
2
3

2. Обновить внутреннее состояние #

Итератор должен помнить, где он сейчас находится.

Например:

class Counter:
    def __init__(self, limit):
        self.current = 0
        self.limit = limit

    def __iter__(self):
        return self

    def __next__(self):
        if self.current >= self.limit:
            raise StopIteration

        value = self.current
        self.current += 1
        return value

Использование:

counter = Counter(3)

print(next(counter))  # 0
print(next(counter))  # 1
print(next(counter))  # 2
print(next(counter))  # StopIteration

Здесь self.current — это внутреннее состояние итератора.

После каждого вызова __next__() оно изменяется:

current = 0 → вернуть 0 → current = 1
current = 1 → вернуть 1 → current = 2
current = 2 → вернуть 2 → current = 3
current = 3 → элементов больше нет → StopIteration

3. Завершить итерацию через StopIteration #

Когда элементы закончились, __next__() не должен возвращать None, False, 0 или другое специальное значение.

Он обязан выбросить исключение:

raise StopIteration

Потому что None, False, 0 — это обычные допустимые значения.

Например, такой итератор вполне может возвращать None как нормальный элемент:

class MyIterator:
    def __iter__(self):
        return self

    def __next__(self):
        return None

Поэтому конец итерации обозначается именно StopIteration, а не возвращаемым значением.

Как это использует for #

Когда ты пишешь:

for item in counter:
    print(item)

Python примерно делает следующее:

iterator = iter(counter)

while True:
    try:
        item = next(iterator)
    except StopIteration:
        break

    print(item)

То есть цикл for вызывает __next__() снова и снова, пока не получит StopIteration.

Полная логика __next__ #

Типичная схема такая:

def __next__(self):
    if элементов_больше_нет:
        raise StopIteration

    value = следующий_элемент
    обновить_внутреннее_состояние
    return value

Практический пример:

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

    def __iter__(self):
        return self

    def __next__(self):
        if self.current >= self.stop:
            raise StopIteration

        value = self.current
        self.current += 1
        return value

Использование:

for number in MyRange(1, 5):
    print(number)

Вывод:

1
2
3
4

Важно #

__next__() не обязан создавать все элементы заранее. Он может вычислять следующий элемент прямо в момент вызова.

Именно поэтому итераторы удобны для больших последовательностей:

class InfiniteCounter:
    def __init__(self):
        self.current = 0

    def __iter__(self):
        return self

    def __next__(self):
        value = self.current
        self.current += 1
        return value

Такой итератор бесконечный:

counter = InfiniteCounter()

print(next(counter))  # 0
print(next(counter))  # 1
print(next(counter))  # 2

Итог #

__next__() в итерационном протоколе выполняет роль метода, который:

1. Возвращает следующий элемент
2. Обновляет состояние итератора
3. Выбрасывает StopIteration, когда элементы закончились

Кратко:

def __next__(self):
    if конец:
        raise StopIteration

    вернуть_следующий_элемент

__iter__() делает объект итератором, а __next__() определяет, как именно этот итератор двигается вперёд.


5. Объект итератор vs итерируемый объект #

Итерируемый объект — объект, по которому можно пройтись в цикле for.

Итератор — объект, который непосредственно выдаёт элементы по одному через __next__().

Главная разница:

итерируемый объект → даёт итератор
итератор → выдаёт элементы

В Python итерируемый объект передаётся в iter(), а iter() возвращает итератор. Сам итератор при вызове next() возвращает следующий элемент или выбрасывает StopIteration, когда элементы закончились.

Итерируемый объект #

Итерируемый объект должен уметь возвращать итератор.

Обычно для этого у него есть метод:

__iter__()

Примеры итерируемых объектов:

list
tuple
str
dict
set
range
file

Например:

numbers = [10, 20, 30]

for number in numbers:
    print(number)

Список numbers — это итерируемый объект.

Но сам список не является итератором:

numbers = [10, 20, 30]

print(next(numbers))

Будет ошибка:

TypeError: 'list' object is not an iterator

Почему? Потому что у списка нет прямой обязанности самому помнить текущую позицию итерации. Список хранит данные, а для обхода создаётся отдельный объект-итератор.

Итератор #

Итератор должен реализовывать два метода:

__iter__()
__next__()

При этом __iter__() у итератора обычно возвращает сам объект:

def __iter__(self):
    return self

А __next__() возвращает следующий элемент:

def __next__(self):
    ...

Когда элементы закончились, __next__() должен выбросить:

raise StopIteration

По документации Python итератор представляет поток данных: повторные вызовы __next__() или next() возвращают следующие элементы, а при окончании данных выбрасывается StopIteration.

Пример со списком #

numbers = [10, 20, 30]

iterator = iter(numbers)

print(iterator)
print(next(iterator))  # 10
print(next(iterator))  # 20
print(next(iterator))  # 30
print(next(iterator))  # StopIteration

Здесь:

numbers

это итерируемый объект.

А:

iterator = iter(numbers)

это уже итератор.

Важное отличие #

Итерируемый объект можно обычно проходить несколько раз:

numbers = [1, 2, 3]

for x in numbers:
    print(x)

for x in numbers:
    print(x)

Вывод будет два раза:

1
2
3
1
2
3

Потому что список каждый раз создаёт новый итератор.

А сам итератор обычно одноразовый:

numbers = [1, 2, 3]

iterator = iter(numbers)

for x in iterator:
    print(x)

for x in iterator:
    print(x)

Вывод:

1
2
3

Второй цикл ничего не выведет, потому что итератор уже исчерпан.

Официальная документация отдельно подчёркивает: контейнер вроде list создаёт новый итератор при каждом вызове iter(), а попытка повторно пройтись по уже использованному итератору вернёт тот же исчерпанный объект.

Таблица различий #

КритерийИтерируемый объектИтератор
НазначениеДаёт возможность обходаНепосредственно выдаёт элементы
Главный метод__iter__()__iter__() и __next__()
Работает с forДаДа
Работает с next() напрямуюОбычно нетДа
Хранит текущую позициюОбычно нетДа
Можно пройти несколько разОбычно даОбычно нет
Примерlist, tuple, str, dict, rangeiter(list), генератор, файловый объект

Проверка на примере #

numbers = [1, 2, 3]

print(hasattr(numbers, "__iter__"))   # True
print(hasattr(numbers, "__next__"))   # False

Список — итерируемый объект, но не итератор.

Теперь создадим итератор:

iterator = iter(numbers)

print(hasattr(iterator, "__iter__"))   # True
print(hasattr(iterator, "__next__"))   # True

Теперь это итератор.

Собственный итерируемый объект #

class MyCollection:
    def __init__(self, items):
        self.items = items

    def __iter__(self):
        return iter(self.items)

Использование:

collection = MyCollection([10, 20, 30])

for item in collection:
    print(item)

Здесь MyCollection — итерируемый объект. Он не сам выдаёт элементы, а возвращает итератор от внутреннего списка.

Собственный итератор #

class MyIterator:
    def __init__(self, items):
        self.items = items
        self.index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.index >= len(self.items):
            raise StopIteration

        value = self.items[self.index]
        self.index += 1
        return value

Использование:

iterator = MyIterator([10, 20, 30])

print(next(iterator))  # 10
print(next(iterator))  # 20
print(next(iterator))  # 30

Здесь MyIterator — именно итератор, потому что он сам хранит позицию и сам выдаёт следующие элементы.

Почему каждый итератор — итерируемый объект #

Итератор обязан иметь __iter__(), который возвращает сам итератор:

def __iter__(self):
    return self

Поэтому любой итератор можно использовать в for:

iterator = iter([1, 2, 3])

for item in iterator:
    print(item)

То есть:

каждый итератор является итерируемым объектом
но не каждый итерируемый объект является итератором

Итог #

Итерируемый объект:
    объект, из которого можно получить итератор через iter()

Итератор:
    объект, который возвращает элементы через next()
    и завершает обход через StopIteration

Самая короткая формула:

iterable = [1, 2, 3]        # итерируемый объект
iterator = iter(iterable)  # итератор

next(iterator)             # 1
next(iterator)             # 2
next(iterator)             # 3

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


6. Что такое генераторы и какие основные виды генераторов существуют? #

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

Основные отличия от итератора:

Создание: Итераторы создаются путем определения класса с методами iter() и next(). Генераторы создаются путем написания обычной функции с использованием выражения yield. Также через gen expr:

generator = (x for i in range(5)) Состояние: Итераторы сохраняют свое состояние с помощью переменных класса. Генераторы сохраняют свое состояние в контексте локальных переменных, которые восстанавливаются при каждом выходе и входе из функции-генератора.

Удобство написания: Функции-генераторы часто легче написать и понять, чем полноценные итераторы, потому что не требуются дополнительные методы и классы.

Значения и ошибки генераторы могут принимать значения и ошибки

Генераторы могут возвращать значения, тогда это значение будет в ошибке StopIteration

Преимущества генератора

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

Улучшенная производительность: Благодаря ленивым вычислениям, генераторы могут значительно ускорить выполнение программы, так как они не тратят время на создание и хранение промежуточных результатов.Это особенно актуально при работе с большими объемами данных. Удобство и читаемость кода: Генераторы позволяют упростить код, избегая громоздких циклов и временных переменных. Это делает код более лаконичным и понятным, что облегчает его поддержку и отладку Обработка бесконечных последовательностей: Генераторы могут быть использованы для создания итераторов, которые генерируют бесконечные последовательности данных, например, последовательность случайных чисел или значения в цикле. Управление состоянием: Генераторы сохраняют свое состояние между вызовами, что позволяет им возобновлять вычисления с того места, где они были остановлены. Это дает больший контроль над процессом генерации данных. В целом, генераторы в Python - это мощный инструмент для оптимизации кода и работы с большими объемами данных, обеспечивая при этом гибкость и удобство в использовании. Генераторы — это расширенные итераторы, которые поддерживают двустороннюю коммуникацию и управление выполнением через три специальных метода.

Метод send() — двусторонняя коммуникация Позволяет не только получать значения из генератора, но и передавать данные внутрь генератора в точку приостановки. Когда генератор приостановлен на yield, метод send() передает значение, которое становится результатом этого yield выражения. Это превращает генератор из пассивного источника данных в интерактивный процесс, который может реагировать на внешние стимулы.

Метод throw() — управление исключениями Позволяет инициировать исключения внутри генератора в точке его приостановки. Вместо того чтобы ждать, пока исключение возникнет естественным образом, можно “бросить” исключение извне. Генератор получает это исключение так, как если бы оно возникло на строке с yield. Это позволяет внешнему коду управлять поведением генератора, прерывать его выполнение или изменять логику работы.

Метод close() — корректное завершение Вызывает внутри генератора специальное исключение GeneratorExit, которое позволяет генератору корректно завершить работу и выполнить необходимую очистку ресурсов. В отличие от простого прекращения использования генератора, close() гарантирует, что блоки finally и менеджеры контекста внутри генератора будут выполнены, что предотвращает утечки ресурсов.

Эти методы превращают генераторы из простых источников данных в управляемые stateful-процессы, которые могут:

Получать обратную связь во время выполнения Обрабатывать внешние команды и прерывания Корректно освобождать ресурсы при принудительном завершении Реализовывать сложные протоколы взаимодействия Это основа для таких продвинутых концепций как корутины и асинхронное программирование, где требуется полноценное взаимодействие между вызывающим кодом и выполняемой задачей.

Основные виды генераторов #

В Python обычно выделяют три основных вида:

1. Генераторные функции
2. Генераторные выражения
3. Асинхронные генераторы

1. Генераторные функции #

Это функции, внутри которых используется yield.

def countdown(n):
    while n > 0:
        yield n
        n -= 1

Использование:

for value in countdown(3):
    print(value)

Вывод:

3
2
1

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

Пример с фильтрацией:

def even_numbers(numbers):
    for number in numbers:
        if number % 2 == 0:
            yield number

Использование:

for number in even_numbers([1, 2, 3, 4, 5, 6]):
    print(number)

Вывод:

2
4
6

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

Генераторное выражение похоже на list comprehension, но не создаёт список сразу.

Список:

squares = [x ** 2 for x in range(5)]

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

squares = (x ** 2 for x in range(5))

Использование:

for value in squares:
    print(value)

Вывод:

0
1
4
9
16

Разница:

lst = [x ** 2 for x in range(1_000_000)]
gen = (x ** 2 for x in range(1_000_000))
list comprehension     → сразу создаёт весь список в памяти
generator expression   → вычисляет элементы по одному

Генераторные выражения удобны для простых преобразований и фильтраций:

even_squares = (x ** 2 for x in range(10) if x % 2 == 0)

3. Асинхронные генераторы #

Асинхронный генератор создаётся через async def и yield.

import asyncio


async def async_counter():
    for i in range(3):
        await asyncio.sleep(1)
        yield i

Использование:

async def main():
    async for value in async_counter():
        print(value)

Асинхронные генераторы используются, когда значения приходят не сразу, а через асинхронные операции:

сетевые запросы
чтение из WebSocket
асинхронное чтение файлов
стриминг данных
работа с очередями

Обычный генератор обходится через:

for item in generator:
    ...

Асинхронный генератор обходится через:

async for item in async_generator:
    ...

yield from #

yield from — это не отдельный вид генератора, а способ делегировать генерацию другому итерируемому объекту.

Без yield from:

def gen():
    for x in [1, 2, 3]:
        yield x

С yield from:

def gen():
    yield from [1, 2, 3]

Это удобно, когда один генератор должен отдавать значения из другого:

def first():
    yield 1
    yield 2


def second():
    yield from first()
    yield 3

Использование:

for value in second():
    print(value)

Вывод:

1
2
3

Когда использовать генераторы #

Генераторы полезны, когда:

данных много
не нужно хранить всё в памяти
значения можно вычислять по одному
нужно построить pipeline обработки данных
последовательность потенциально бесконечная

Пример бесконечного генератора:

def counter():
    value = 0

    while True:
        yield value
        value += 1

Использование:

gen = counter()

print(next(gen))  # 0
print(next(gen))  # 1
print(next(gen))  # 2

Такой генератор не создаёт бесконечный список. Он просто хранит текущее состояние и выдаёт следующее число по запросу.

Генератор vs список #

numbers_list = [x for x in range(5)]
numbers_gen = (x for x in range(5))
КритерийСписокГенератор
Хранит все элементыДаНет
Вычисляет значенияСразуПо одному
Можно пройти несколько разДаОбычно нет
Поддерживает len()ДаНет
Поддерживает индексациюДаНет
Экономит памятьНетДа

Пример:

gen = (x for x in range(3))

for x in gen:
    print(x)

for x in gen:
    print(x)

Вывод:

0
1
2

Второй цикл ничего не выведет, потому что генератор уже исчерпан.

Итог #

Генератор — это специальный итератор, который создаёт значения лениво.

Основные виды:

Генераторная функция:
    def func():
        yield value

Генераторное выражение:
    (x for x in iterable)

Асинхронный генератор:
    async def func():
        yield value

Коротко:

Генератор не хранит всю последовательность.
Он запоминает состояние выполнения и выдаёт значения по одному.


7. Генераторы vs итераторы #

Итератор — это общий механизм обхода элементов через __next__().

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

каждый генератор — итератор
но не каждый итератор — генератор

Итератор #

Итератор — это объект, который реализует итерационный протокол:

__iter__()
__next__()

Пример собственного итератора:

class Counter:
    def __init__(self, limit):
        self.current = 0
        self.limit = limit

    def __iter__(self):
        return self

    def __next__(self):
        if self.current >= self.limit:
            raise StopIteration

        value = self.current
        self.current += 1
        return value

Использование:

counter = Counter(3)

print(next(counter))  # 0
print(next(counter))  # 1
print(next(counter))  # 2

Здесь мы вручную описываем:

как хранить состояние
как получить следующий элемент
когда остановить итерацию

Генератор #

Генератор позволяет сделать то же самое короче, через yield.

def counter(limit):
    current = 0

    while current < limit:
        yield current
        current += 1

Использование:

gen = counter(3)

print(next(gen))  # 0
print(next(gen))  # 1
print(next(gen))  # 2

Здесь Python сам создаёт объект-итератор и сам управляет состоянием функции между вызовами next().

Главное отличие #

Итератор через класс:

class Counter:
    def __iter__(self):
        return self

    def __next__(self):
        ...

Генератор:

def counter():
    yield ...

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

Таблица различий #

КритерийИтераторГенератор
Что этоОбщий объект для обходаЧастный вид итератора
Как создаётсяОбычно через класс с __iter__ и __next__Через yield или генераторное выражение
Кто хранит состояниеТы сам в атрибутах объектаPython автоматически
Нужно писать __next__ДаНет
Нужно писать StopIterationДаОбычно нет
Можно использовать в forДаДа
Можно использовать с next()ДаДа
Обычно одноразовыйДаДа

Сравнение на одном примере #

Итератор вручную:

class EvenNumbers:
    def __init__(self, limit):
        self.current = 0
        self.limit = limit

    def __iter__(self):
        return self

    def __next__(self):
        if self.current >= self.limit:
            raise StopIteration

        value = self.current
        self.current += 2
        return value

То же самое через генератор:

def even_numbers(limit):
    current = 0

    while current < limit:
        yield current
        current += 2

Использование одинаковое:

for number in even_numbers(10):
    print(number)

Вывод:

0
2
4
6
8

Генератор сам является итератором #

def numbers():
    yield 1
    yield 2
    yield 3


gen = numbers()

print(iter(gen) is gen)       # True
print(hasattr(gen, "__next__"))  # True

То есть генераторный объект поддерживает и iter(), и next().

Важный момент: генератор одноразовый #

gen = (x for x in range(3))

for x in gen:
    print(x)

for x in gen:
    print(x)

Вывод:

0
1
2

Второй цикл ничего не выведет, потому что генератор уже исчерпан.

То же самое обычно верно и для обычных итераторов.

Когда использовать итератор через класс #

Класс-итератор уместен, когда нужна более сложная объектная логика:

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

Пример:

class PaginatedAPIIterator:
    def __init__(self, client, page_size):
        self.client = client
        self.page_size = page_size
        self.page = 1

В таких случаях класс может быть понятнее, чем большая генераторная функция.

Когда использовать генератор #

Генератор лучше, когда нужно просто выдавать элементы по одному:

читать файл построчно
фильтровать данные
обрабатывать большой поток
строить pipeline
генерировать последовательность
не хранить всё в памяти

Пример:

def read_lines(path):
    with open(path, "r", encoding="utf-8") as file:
        for line in file:
            yield line.strip()

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

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

squares = (x ** 2 for x in range(5))

Это тоже генератор:

print(next(squares))  # 0
print(next(squares))  # 1
print(next(squares))  # 4

Итог #

Итератор:
    общий протокол обхода элементов
    требует __iter__ и __next__

Генератор:
    специальный итератор
    создаётся через yield или генераторное выражение
    автоматически сохраняет состояние выполнения

Самая короткая формула:

итератор — механизм
генератор — удобный синтаксис для создания итератора


8. Почему range() в Python не хранит все элементы сразу? #

Почему range() не хранит все элементы сразу #

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

start
stop
step

Например:

r = range(0, 1_000_000_000, 2)

Python не создаёт миллиард чисел в памяти. Он хранит примерно такую информацию:

start = 0
stop  = 1_000_000_000
step  = 2

А конкретный элемент вычисляется только когда он нужен:

r[5]

Вычисление:

start + step * index
0 + 2 * 5 = 10

Официальная документация прямо указывает, что преимущество range перед list и tuple в том, что объект range занимает одинаково малый объём памяти независимо от размера диапазона, потому что хранит только start, stop, step, а элементы вычисляет по необходимости.

Почему так сделано #

Главная причина — экономия памяти.

range(10)

Логически означает:

0, 1, 2, 3, 4, 5, 6, 7, 8, 9

Но хранить это не нужно, потому что все значения можно получить по формуле:

r[i] = start + step * i

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

Сравнение с list #

r = range(1_000_000)
lst = list(range(1_000_000))

Разница принципиальная:

range хранит правило построения чисел
list хранит сами числа

То есть:

range(1_000_000)

хранит:

start=0, stop=1000000, step=1

А:

list(range(1_000_000))

создаёт в памяти все элементы:

[0, 1, 2, 3, ..., 999999]

Важный момент #

range — это не генератор.

Он похож на «ленивую» структуру, потому что не хранит все элементы, но технически это объект последовательности. Он поддерживает индексацию, проверку in, срезы и отрицательные индексы. Документация указывает, что range реализует collections.abc.Sequence и поддерживает такие операции последовательностей.

Пример:

r = range(0, 20, 2)

print(r[5])     # 10
print(10 in r)  # True
print(r[:5])    # range(0, 10, 2)
print(r[-1])    # 18

Итог #

range() не хранит все элементы сразу, потому что ему это не нужно.

Он хранит только:

начало
конец
шаг

А числа вычисляет по формуле в момент обращения. Поэтому range(10) и range(10_000_000_000) занимают примерно одинаково мало памяти, в отличие от списка.


9. Что такое enumerate в Python? #

Enumerate используют, чтобы в цикле одновременно получить и индекс элемента, и само значение, не занимаясь ручным счётчиком.​

Enumerate(iterable, start=0) возвращает итератор, который на каждой итерации даёт кортеж вида (индекс, элемент).​Индекс по умолчанию начинается с 0, но параметром start можно задать другое начальное значение, например 1.​

Типичный вариант в цикле for:


for i, fruit in enumerate(fruits, start=1): print(i, fruit) ``` Здесь на каждой итерации i  это номер, а fruit  значение списка.

Enumerate удобен, когда нужно:

Перебирать список/строку/кортеж и параллельно знать индекс элемента.

Избавиться от шаблона вида for i in range(len(seq)): и сделать код короче и читабельнее.

Функция работает с любыми итерируемыми объектами (списки, строки, кортежи, генераторы и т.п.), а не только с индексируемыми последовательностями.


10. Как работает else в цикле for #

В Python блок else в циклах for и while выполняется, если цикл завершился «естественным» образом, то есть без выхода через оператор break. Если цикл был прерван break, то else не выполняется.

Принцип работы:

Цикл выполняет все итерации (или пока условие истинно для while).

Если цикл завершился полностью, выполняется else.

Если в процессе был вызван break, else пропускается.

Примеры:

for i in range(3): print(i) else: print(“Цикл завершён без break”) Выведет:

text 0 1 2 Цикл завершён без break А вот с break:

for i in range(3): if i == 1: break print(i) else: print(“Цикл завершён без break”) Выведет:

text 0 (блок else не выполнится).

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

То же работает с while-циклами: else выполняется, когда условие цикла становится ложным, а не был применён break.

Таким образом, else в циклах — это блок, который выполняется после успешного окончания цикла без досрочного выхода, позволяя удобно обрабатывать ситуацию «ничего не найдено» или «цикл завершён полностью»


11. Как сделать класс-генератор в Python? #

В Python “класс-генератор” обычно делают одним из двух способов:

  1. Через __iter__() с yield — класс является итерируемым объектом.

  2. Через __iter__() + __next__() — класс сам является итератором.

Официально генератором становится функция или метод, внутри которого есть yield. При вызове такая функция возвращает объект-итератор, а выполнение продолжается до следующего yield при вызове __next__(); при завершении поднимается StopIteration.


Вариант 1: класс с генераторным __iter__ #

Это самый удобный вариант.

class Countdown:
    def __init__(self, start):
        self.start = start

    def __iter__(self):
        current = self.start

        while current > 0:
            yield current
            current -= 1


counter = Countdown(3)

for number in counter:
    print(number)

Вывод:

3
2
1

Здесь __iter__() — обычный метод класса, но внутри него есть yield, поэтому он становится генераторным методом. Python сам создаёт генераторный объект, который уже имеет __iter__() и __next__(). Это прямо описано в документации: если __iter__() контейнера реализован как генератор, он автоматически возвращает iterator/generator object с нужными методами.


Что происходит пошагово #

counter = Countdown(3)

Создаётся обычный объект класса.

for number in counter:

Цикл for вызывает:

iter(counter)

А это вызывает:

counter.__iter__()

Так как внутри __iter__() есть yield, метод возвращает генератор.

Дальше for много раз вызывает у генератора:

__next__()

Каждый вызов идёт до следующего yield.


Вариант 2: класс-итератор вручную #

Можно реализовать протокол итератора самому:

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

    def __iter__(self):
        return self

    def __next__(self):
        if self.current <= 0:
            raise StopIteration

        value = self.current
        self.current -= 1
        return value


counter = Countdown(3)

for number in counter:
    print(number)

Вывод:

3
2
1

Итератор по протоколу обязан иметь два метода: __iter__(), который возвращает сам iterator object, и __next__(), который возвращает следующий элемент или выбрасывает StopIteration, если элементы закончились.


Главное отличие двух вариантов #

__iter__() с yield #

class Countdown:
    def __init__(self, start):
        self.start = start

    def __iter__(self):
        current = self.start
        while current > 0:
            yield current
            current -= 1

Плюсы:

проще
меньше кода
не нужно самому писать __next__
каждая новая итерация начинается заново

Пример:

counter = Countdown(3)

print(list(counter))
print(list(counter))

Вывод:

[3, 2, 1]
[3, 2, 1]

__iter__() + __next__() #

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

    def __iter__(self):
        return self

    def __next__(self):
        if self.current <= 0:
            raise StopIteration

        value = self.current
        self.current -= 1
        return value

Минусы:

объект хранит текущее состояние итерации
после одного прохода он уже исчерпан

Пример:

counter = Countdown(3)

print(list(counter))
print(list(counter))

Вывод:

[3, 2, 1]
[]

Потому что counter сам является итератором и после первого прохода его current уже дошёл до 0.


Практический вывод #

Для большинства случаев лучше так:

class MyRange:
    def __init__(self, start, stop):
        self.start = start
        self.stop = stop

    def __iter__(self):
        current = self.start

        while current < self.stop:
            yield current
            current += 1


numbers = MyRange(1, 5)

print(list(numbers))
print(list(numbers))

Вывод:

[1, 2, 3, 4]
[1, 2, 3, 4]

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

Итог #

Класс-генератор в Python обычно делают через __iter__ с yield.

Минимальный шаблон:

class MyGenerator:
    def __iter__(self):
        yield value

А класс-итератор вручную:

class MyIterator:
    def __iter__(self):
        return self

    def __next__(self):
        ...

Первый вариант чаще чище и удобнее. Второй нужен, когда требуется полностью вручную контролировать состояние итератора.


12. Какие ленивые объекты (вычисления) есть в Python? #

Коротко #

В Python нет общей ленивой модели вычислений, как в Haskell. Большинство выражений вычисляются сразу. Но в Python есть много объектов, которые вычисляют или отдают значения только по запросу: при for, next(), list(), sum() и других операциях потребления.

Главная база ленивости в Python — это итераторы. Итератор представляет поток данных и возвращает элементы по одному через __next__(), а при окончании выбрасывает StopIteration. Итераторы не обязаны быть конечными.


1. Генераторы #

Генераторная функция #

def numbers():
    print("start")
    yield 1
    yield 2
    yield 3


gen = numbers()

print(gen)
print(next(gen))
print(next(gen))

Вывод:

<generator object numbers at ...>
start
1
2

Функция не выполняется при вызове numbers(). Она возвращает генераторный объект. Код внутри начинает выполняться только при next(gen).

yield внутри функции делает её генераторной функцией; при работе она возвращает значения постепенно, а не строит весь результат заранее.


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

gen = (x * x for x in range(5))

print(gen)
print(next(gen))
print(next(gen))

Вывод:

<generator object <genexpr> at ...>
0
1

Это ленивый аналог list comprehension.

# eager: сразу строит список
items = [x * x for x in range(5)]

# lazy: создаёт генератор
items = (x * x for x in range(5))

Документация прямо указывает: в генераторном выражении часть выражений вычисляется лениво — когда итератор запрашивает следующее значение.


3. range #

range ленивый в смысле хранения данных.

r = range(1_000_000_000)

print(r[0])
print(r[10])
print(r[-1])

range не хранит миллиард чисел. Он хранит только start, stop, step, а отдельные элементы и поддиапазоны вычисляет по необходимости. Поэтому range занимает небольшой фиксированный объём памяти независимо от длины диапазона.

Важно:

range(10)

это не итератор, а immutable sequence. Но при проходе по нему Python создаёт итератор.


4. Встроенные ленивые функции #

map #

items = map(lambda x: x * 2, [1, 2, 3])

print(items)
print(next(items))
print(next(items))

map() возвращает итератор, который применяет функцию к элементам только при проходе.


filter #

items = filter(lambda x: x > 2, [1, 2, 3, 4])

print(next(items))
print(next(items))

filter() тоже возвращает итератор. Он не создаёт новый список сразу.


zip #

items = zip([1, 2, 3], ["a", "b", "c"])

print(next(items))
print(next(items))

zip() возвращает итератор кортежей. В документации прямо сказано, что zip() ленивый: элементы не обрабатываются до начала итерации.


enumerate #

items = enumerate(["a", "b", "c"], start=1)

print(next(items))
print(next(items))

enumerate() возвращает объект, чей __next__() отдаёт пару: индекс и значение из исходного iterable.


reversed #

items = reversed([1, 2, 3])

print(next(items))
print(next(items))

reversed() возвращает обратный итератор, а не список.


5. itertools #

Модуль itertools почти полностью построен вокруг ленивых итераторов.

Примеры:

from itertools import count, islice

numbers = count(10)

print(next(numbers))
print(next(numbers))
print(next(numbers))

count() может создавать бесконечный поток значений:

10
11
12

Чтобы взять только часть:

from itertools import count, islice

items = islice(count(1), 5)

print(list(items))

Вывод:

[1, 2, 3, 4, 5]

Важные ленивые инструменты из itertools:

itertools.count()
itertools.repeat()
itertools.cycle()
itertools.chain()
itertools.islice()
itertools.takewhile()
itertools.dropwhile()
itertools.starmap()
itertools.groupby()
itertools.zip_longest()

Документация описывает itertools как функции, создающие итераторы для эффективных циклов. Многие из них выдают значения постепенно, но есть нюансы: например, cycle() сохраняет элементы, tee() может требовать дополнительную память, а product() заранее потребляет входные iterable в пулы.


6. Файловые объекты #

Файл можно читать лениво построчно:

with open("data.txt", encoding="utf-8") as file:
    for line in file:
        print(line)

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


7. re.finditer #

import re

text = "one two three"

matches = re.finditer(r"\w+", text)

for match in matches:
    print(match.group())

re.finditer() возвращает итератор Match-объектов, а не список всех совпадений. В отличие от re.findall(), он удобнее для больших строк, когда не нужно сразу хранить все совпадения.


8. os.scandir #

import os

with os.scandir(".") as entries:
    for entry in entries:
        print(entry.name)

os.scandir() возвращает итератор объектов os.DirEntry, то есть элементы директории выдаются постепенно. Это обычно эффективнее, чем сначала получать полный список через os.listdir(), особенно если нужны сведения о типе файла.


9. Ленивые свойства: property и cached_property #

property #

class User:
    @property
    def full_name(self):
        print("computed")
        return "John Smith"


user = User()

print(user.full_name)
print(user.full_name)

property вычисляется при обращении к атрибуту, а не при создании объекта. Но без кэша она будет вычисляться каждый раз.


functools.cached_property #

from functools import cached_property


class DataSet:
    def __init__(self, values):
        self.values = values

    @cached_property
    def total(self):
        print("computed")
        return sum(self.values)


data = DataSet([1, 2, 3])

print(data.total)
print(data.total)

Вывод:

computed
6
6

cached_property вычисляет значение один раз при первом обращении, затем сохраняет его как обычный атрибут объекта. Повторные обращения берут уже сохранённое значение.


10. Кэшируемые функции: functools.cache / lru_cache #

Это не совсем ленивый объект, но ленивое вычисление по требованию.

from functools import lru_cache


@lru_cache
def slow_square(x):
    print("computed")
    return x * x


print(slow_square(5))
print(slow_square(5))

Вывод:

computed
25
25

Функция вычисляется при первом вызове с конкретными аргументами, а затем результат берётся из кэша. В документации functools это описано как механизм повторного использования ранее вычисленных значений.


Что не является ленивым #

Списки #

items = [x * x for x in range(10)]

Список строится сразу.


list(), tuple(), set() #

items = list(map(lambda x: x * 2, range(10)))

map() ленивый, но list() сразу потребляет весь итератор.


sorted() #

items = sorted(range(10))

sorted() должен собрать элементы, чтобы отсортировать их.


sum(), max(), min() #

total = sum(x * x for x in range(10))

Генератор внутри ленивый, но sum() сразу проходит по нему до конца.


Итоговая таблица #

Объект / функцияЛенивость
generator functionвыдаёт значения по yield
generator expressionвычисляет элементы при итерации
rangeне хранит все числа, вычисляет элементы по индексу
mapприменяет функцию при итерации
filterфильтрует при итерации
zipсобирает пары при итерации
enumerateдобавляет индекс при итерации
reversedвозвращает обратный итератор
itertools.*в основном ленивые итераторы
file objectчитает строки постепенно
re.finditerотдаёт совпадения постепенно
os.scandirотдаёт элементы директории постепенно
cached_propertyвычисляет атрибут при первом обращении
lru_cache / cacheвычисляет результат при первом вызове с аргументами

Главный вывод #

В Python ленивость чаще всего связана с итераторами:

map(...)
filter(...)
zip(...)
enumerate(...)
generator
itertools...

Пока ты не начал проход через for, next(), list(), sum() или похожий потребитель, реальные значения обычно не вычисляются.