Итераторы и Генераторы #
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 (истощения) нельзя использовать снова
Ключевые различия
| Характеристика | Iterable | Iterator |
|---|---|---|
| Многократное использование | ✅ Да | ❌ Нет |
| Состояние | Не хранит состояние итерации | Хранит текущее состояние |
| Методы | __iter__() | __iter__() и __next__() |
| Примеры | list, tuple, dict | iter(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, range | iter(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 “класс-генератор” обычно делают одним из двух способов:
Через
__iter__()сyield— класс является итерируемым объектом.Через
__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() или похожий потребитель, реальные значения обычно не вычисляются.