Итераторы, генераторы и контекстные менеджеры
Итераторы, генераторы и контекстные менеджеры
Итератор — это объект, который выдаёт элементы по одному и «помнит», на каком месте остановился. Генератор — самый простой способ такой итератор создать.
Оба инструмента помогают работать с большими объёмами данных, не загружая всё в память: элементы вычисляются лениво — только в момент запроса.
Как пагинация. Представим себе книгу вместо стопки распечатанных страниц:
- Стопка страниц (список) — сразу занимает место на столе, нужно знать все страницы заранее.
- Итератор как чтение книги — в руках одна страница, остальные — в источнике. Ты не знаешь заранее, сколько всего страниц, но можешь читать по одной.
Читаем файл в 10 ГБ — итератор не загружает весь файл в память, а выдаёт строку за строкой. Список бы убил всю память.
Три термина, которые нельзя путать
Итератор представляет собой объект, который реализует протокол итерации и позволяет последовательно обходить элементы коллекции или потока данных, при этом он хранит состояние текущей позиции в процессе обхода и знает, как получить следующий элемент, а когда элементы заканчиваются, итератор сигнализирует об этом с помощью специального исключения, что делает его универсальным механизмом для поочередного доступа к данным без необходимости загружать их все в память одновременно.
Генератор представляет собой особый тип итератора, который создается с использованием функции, содержащей ключевое слово yield, или с помощью генераторного выражения, и который генерирует значения последовательно одно за другим по запросу, при этом он не хранит всю последовательность в памяти, а вычисляет каждое следующее значение только в момент обращения, что делает его идеальным инструментом для работы с бесконечными последовательностями и большими объемами данных.
Итерируемый представляет собой любой объект, от которого можно получить итератор, то есть объект, реализующий специальный метод, возвращающий итератор, и который может быть использован в цикле for или передан в функцию iter, при этом к итерируемым объектам относятся все встроенные коллекции, такие как списки, кортежи, строки и словари, а также многие другие объекты, поддерживающие итерационный протокол.
| Термин | Что это | Методы | Можно пройти for дважды? |
|---|---|---|---|
| Iterable (итерируемый) | Объект, по которому можно пройти циклом for | __iter__() | Да — если каждый раз создаётся новый итератор |
| Iterator (итератор) | Объект, который сейчас перебирает элементы | __iter__() + __next__() | Нет — одноразовый, после исчерпания пуст |
| Generator (генератор) | Итератор, созданный через yield или (...) | те же, что у итератора | Нет — одноразовый |
Каждый генератор — итератор, но не каждый итератор — генератор. Список, строка, кортеж — итерируемые, но не итераторы. Функция с yield — и итератор, и генератор.
Итераторы
Протокол итерации представляет собой формальный набор правил и методов, определяющих, как объект должен вести себя для поддержки итерации, и включает в себя метод, возвращающий итератор, для итерируемых объектов и метод, возвращающий следующий элемент или выбрасывающий исключение при завершении, для самих итераторов, причем соблюдение этого протокола позволяет объектам интегрироваться в стандартные механизмы перебора языка.
Итератор реализует протокол итерации — два метода:
__iter__()— возвращает сам объект (self).__next__()— возвращает следующий элемент. Если элементы закончились — выбрасываетStopIteration.
Self представляет собой первый параметр методов экземпляра в классах Python, который является ссылкой на сам объект, для которого вызывается метод, и который используется для доступа к атрибутам и другим методам этого объекта внутри определения класса, причем это соглашение об именовании является общепринятым, хотя формально можно использовать любое имя, и этот параметр передается автоматически при вызове метода, не требуя явной передачи от разработчика.
StopIteration представляет собой встроенное исключение, которое выбрасывается итератором для сигнализации о том, что в последовательности больше нет элементов для перебора, и которое автоматически перехватывается циклом for и другими конструкциями, использующими итерацию, чтобы корректно завершить процесс обхода, при этом разработчик может вручную обрабатывать это исключение при использовании функции next для управления итерацией.
Yield представляет собой ключевое слово, которое используется внутри функции для превращения её в функцию-генератор, и которое приостанавливает выполнение функции, сохраняя все её локальные переменные и состояние, и возвращает текущее значение вызывающей стороне, при этом при следующем обращении к генератору выполнение возобновляется с точки останова, что позволяет создавать элегантные и эффективные последовательности значений.
Итератор «помнит» позицию обхода. Именно поэтому он одноразовый: второй проход по тому же объекту ничего не даст.
class MyNumbers:
def __init__(self):
self.num = 1
def __iter__(self):
return self
def __next__(self):
if self.num <= 3:
val = self.num
self.num += 1
return val
raise StopIteration
my_iter = MyNumbers()
print(next(my_iter)) # 1
print(next(my_iter)) # 2
print(next(my_iter)) # 3
# next(my_iter) # StopIteration
Разбор примера:
__iter__возвращаетself— объект одновременно и итерируемый, и итератор.__next__отдаёт значение и сдвигает счётчик; при исчерпании —StopIteration.next(my_iter)— ручной запрос следующего элемента, без циклаfor.
Код ITЗагрузка примера кода…
Разбор примера CountDown:
__iter__возвращаетself— объект работает как собственный итератор.__next__выбрасываетStopIteration, когдаcurrent < 0.- До этого возвращается текущее значение, счётчик уменьшается на 1.
CountDown— одноразовый итератор: второйfor n in CountDown(3)сработает только если создать новый экземпляр.
Iterable и Iterator — главное различие
Итерируемый объект (Iterable) — любой объект, который можно перебрать в for. У него есть __iter__(), который возвращает новый итератор. Примеры: list, str, tuple, dict, set.
Итератор (Iterator) — объект с __iter__() (возвращает self) и __next__(). Он хранит состояние текущего обхода. После исчерпания повторные вызовы next() бросают StopIteration.
data = [10, 20, 30] # Iterable — можно обходить многократно
it = iter(data) # Iterator — одноразовый поток
print(next(it)) # 10
print(next(it)) # 20
# print(next(it)) # 30, затем StopIteration
for x in data: print(x) # работает снова — создаётся новый итератор
for x in it: print(x) # ничего не выведет — итератор уже пуст
Цикл for — синтаксический сахар
Привычный for x in collection Python разворачивает так:
_iterator = iter(collection) # вызов __iter__()
while True:
try:
x = next(_iterator) # вызов __next__()
except StopIteration:
break
# тело цикла
Именно StopIteration — сигнал «элементы закончились». Цикл for перехватывает его автоматически; при ручном next() вы получите исключение сами.
Встроенные функции sum, list, tuple, max и выражение x in collection внутри работают по тому же принципу: iter() → next() в цикле.
Три правильных способа реализовать __iter__
Выбор зависит от цели: нужен многоразовый обход (Iterable) или одноразовый поток (Iterator).
Вариант 1 — yield внутри __iter__ (самый частый)
Если объект должен перебираться в for многократно, yield в __iter__ создаёт свежий генератор-итератор при каждом вызове:
class Garage:
def __init__(self):
self.cars = ["Tesla", "BMW", "Audi"]
def __iter__(self):
for car in self.cars:
yield car
my_garage = Garage()
for car in my_garage: print(car) # первый проход
for car in my_garage: print(car) # второй проход — работает
Вариант 2 — делегирование через iter() (самый быстрый)
Если класс оборачивает готовую коллекцию — не изобретайте велосипед:
class Team:
def __init__(self):
self.players = ["Иван", "Сергей", "Анна"]
def __iter__(self):
return iter(self.players)
our_team = Team()
for player in our_team:
print(player)
Вариант 3 — отдельный класс-итератор (классическое ООП)
Сложную логику обхода выносят в отдельный класс. Iterable при каждом __iter__ создаёт новый экземпляр:
class AlphabetIterator:
def __init__(self, letters):
self.letters = letters
self.index = 0
def __iter__(self):
return self
def __next__(self):
if self.index >= len(self.letters):
raise StopIteration
result = self.letters[self.index]
self.index += 1
return result
class Alphabet:
def __init__(self):
self.letters = ["A", "B", "C"]
def __iter__(self):
return AlphabetIterator(self.letters)
abc = Alphabet()
print(list(abc)) # ['A', 'B', 'C']
print(list(abc)) # ['A', 'B', 'C'] — новый итератор каждый раз
Главное правило:
- Итератор — есть
__iter__(возвращаетself) и__next__. Одноразовый. - Итерируемый — есть только
__iter__, который возвращает другой, свежий объект с__next__. Многоразовый.
Генератор
Генератор — самый простой способ создать итератор. Не нужно вручную писать __iter__ и __next__ — Python делает это за вас, когда видит yield.
Вместо return (который завершает функцию) yield замораживает состояние — локальные переменные, позицию в коде — и отдаёт значение. Следующий next() продолжает с того же места.
Генераторы реализуют ленивую загрузку (lazy evaluation) — значения появляются по мере запроса, а не все сразу.
def count_up_to(n):
i = 1
while i <= n:
yield i
i += 1
gen = count_up_to(3)
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # 3
# next(gen) # StopIteration
Разбор примера:
yieldвозвращает значение и «замораживает» состояние функции.- Повторный
next(gen)продолжает выполнение с места послеyield. - Когда условие
i <= nстановится ложным, генератор завершается. - Следующий
next(...)после завершения вызываетStopIteration.
Генераторы можно использовать в for:
for value in count_up_to(5):
print(value)
Преимущества ленивой загрузки — экономия памяти (не хранятся все элементы), работа с бесконечными последовательностями, ранний доступ к первому элементу.
Пример бесконечного генератора:
def endless_counter():
n = 0
while True:
yield n
n += 1
Два способа создать генератор
1. Функция-генератор (с yield) — показан выше. Python автоматически оборачивает такую функцию в объект-генератор с __iter__ и __next__. Функция-генератор представляет собой функцию, которая вместо ключевого слова return использует одно или несколько ключевых слов yield для возврата значений, и которая при вызове не выполняет свой код сразу, а возвращает объект-генератор, который затем может быть использован для поочередного получения значений, причем каждый вызов метода next заставляет функцию выполняться до следующего yield, после чего выполнение снова приостанавливается.
2. Генераторное выражение (generator expression) — аналог спискового включения, но в круглых скобках. Создаёт генератор в одну строку:
gen_exp = (x * 2 for x in range(1, 4))
for num in gen_exp:
print(num) # 2, 4, 6
Генераторное выражение представляет собой компактный синтаксический конструктор, похожий на списковое включение, но заключенный в круглые скобки, который создает объект-генератор, вычисляющий элементы лениво по требованию, и который позволяет сэкономить память при работе с большими последовательностями, поскольку значения генерируются непосредственно в момент итерации, а не создаются все сразу в виде готового списка.
Разница между [...] и (...) принципиальна:
[x**2 for x in range(1_000_000)]— сразу создаст миллион объектов в памяти.(x**2 for x in range(1_000_000))— потребляет константный объём памяти.
Выражения-генераторы удобно передавать в функции, ожидающие итератор:
total = sum(x**2 for x in range(10))
data = [10, -5, 8, 3]
max_value = max(x for x in data if x > 0)
Списковые включения — это не генераторы
Списковое включение (list comprehension) — [expr for item in iterable if condition] — сразу строит список в памяти. Списковые включения представляют собой элегантный способ создания новых списков из существующих итерируемых объектов с использованием краткого синтаксиса в квадратных скобках, при котором каждый элемент новой коллекции вычисляется на основе элемента исходной последовательности, причем поддерживается фильтрация с помощью условия, и все это записывается в одной строке, что делает код более декларативным и выразительным по сравнению с традиционными циклами. В разговорной речи его иногда называют «генератором списка», но технически это не генератор:
squares = [x**2 for x in range(5)] # [0, 1, 4, 9, 16] — готовый список
evens = [x for x in range(10) if x % 2 == 0]
Разбор примера:
- Квадратные скобки — результат материализуется сразу, все значения в памяти.
if x % 2 == 0фильтрует элементы (необязательная часть).- Компактнее ручного цикла с
append, но не ленивое.
Не используйте списковые включения ради побочных эффектов (вызов функций без сохранения результата) — это ухудшает читаемость.
Для сравнения — генераторное выражение:
squares_gen = (x**2 for x in range(5))
print(type(squares_gen)) # <class 'generator'>
print(next(squares_gen)) # 0 — только первый элемент вычислен
Ключевое слово yield from позволяет делегировать часть генерации другой функции-генератору.
def sub_generator():
yield "a"
yield "b"
def main_generator():
yield 1
yield from sub_generator() # вставляет значения
yield 2
for item in main_generator():
print(item) # 1, 'a', 'b', 2
Разбор примера:
yield from sub_generator()делегирует выдачу значений вложенному генератору.main_generatorсначала отдаёт1, затем все значения изsub_generator, затем2.- Это эквивалентно циклу
for x in sub_generator(): yield x, но короче и чище.
yield from особенно полезен при работе с вложенными итерациями, деревьями, асинхронным кодом (где используется для await-подобного поведения).
Контекстный менеджер
Контекстный менеджер — это объект, определяющий поведение перед входом в блок кода и после его завершения, независимо от того, завершился ли блок нормально или с исключением.
Контекстный менеджер with представляет собой конструкцию, предназначенную для управления ресурсами, которая гарантирует корректное выполнение подготовительных и завершающих операций, таких как открытие и закрытие файлов или захват и освобождение блокировок, при этом код внутри блока with выполняется после настройки контекста, а выход из блока запускает завершающие действия даже в случае возникновения исключения, что делает управление ресурсами надежным и безопасным.
Он реализует протокол:
__enter__()— вызывается при входе в блокwith.__exit__(exc_type, exc_value, traceback)— вызывается при выходе из блока, даже если произошло исключение.
Основная цель — гарантированное освобождение ресурсов:
- закрытие файлов;
- соединений с БД;
- разблокировка мьютексов;
- т.п.
Синтаксис with:
with open('file.txt') as f:
content = f.read()
# Файл автоматически закрывается здесь, даже если было исключение
Разбор примера:
open(...)возвращает объект файла, который выступает контекстным менеджером.- При входе в
withвызывается__enter__, при выходе —__exit__. - Поэтому ресурс закрывается гарантированно даже при ошибке внутри блока.
- Это безопаснее ручного
open/close.
Эквивалентно:
f = open('file.txt')
try:
content = f.read()
finally:
f.close()
Но with делает код более читаемым и надёжным.
itertools
Модуль itertools предоставляет набор высокоэффективных инструментов для работы с итераторами. Его функции возвращают ленивые итераторы, что делает их идеальными для обработки больших или бесконечных потоков данных.
Бесконечные итераторы:
from itertools import count, cycle, repeat
for n in count(1): # 1, 2, 3, ...
if n > 3: break
print(n)
for c in cycle('AB'): # A, B, A, B, ...
pass
Терминирующие итераторы представляют собой группу функций из модуля itertools, которые принимают один или несколько итераторов и возвращают новый итератор, который завершается после обработки всех элементов, включая такие функции, как accumulate для накопления сумм, chain для объединения последовательностей, compress для фильтрации по булевой маске и takewhile для взятия элементов, пока условие истинно, и эти функции позволяют выполнять различные операции над конечными последовательностями данных.
Терминирующие итераторы:
from itertools import takewhile, dropwhile, chain
list(takewhile(lambda x: x < 5, [1, 4, 6, 4, 1])) # [1, 4]
list(chain([1, 2], [3, 4])) # [1, 2, 3, 4]
Комбинаторные итераторы представляют собой группу функций из модуля itertools, которые генерируют все возможные комбинации, перестановки и декартовы произведения элементов из входных итерируемых объектов, включая функции product для декартова произведения, permutations для всех возможных упорядоченных перестановок, combinations для всех возможных комбинаций без повторений и combinations_with_replacement для комбинаций с повторениями, и эти функции широко используются в задачах оптимизации, перебора вариантов и генерации тестовых данных.
Комбинаторные итераторы
from itertools import product, permutations, combinations
list(product('AB', repeat=2)) # [('A','A'), ('A','B'), ...]
list(combinations('ABC', 2)) # [('A','B'), ('A','C'), ('B','C')]
Практические шаблоны использования
Эти конструкции особенно полезны в production-коде, где важны память и надёжность:
- Итераторы — потоковая обработка логов и больших файлов.
- Генераторы — поэтапные ETL-пайплайны и lazy-преобразования.
- Контекстные менеджеры — безопасная работа с файлами, сокетами, транзакциями и блокировками.
Пример ленивого пайплайна:
def read_lines(path):
with open(path, "r", encoding="utf-8") as f:
for line in f:
yield line.strip()
def only_errors(lines):
for line in lines:
if "ERROR" in line:
yield line
errors = only_errors(read_lines("app.log"))
for item in errors:
print(item)
Разбор примера:
read_linesчитает файл построчно и отдаёт строки черезyield.only_errorsпринимает поток строк и пропускает дальше только те, где есть"ERROR".- Переменная
errorsхранит ленивый пайплайн, а не готовый список. - Память расходуется экономно, потому что обработка идёт по одной строке.
Код обрабатывает данные по одной строке и не загружает весь файл в память.
Частые ошибки
- Повторный проход по исчерпанному итератору/генератору —
for x in genвторой раз ничего не выдаст. Создайте новый генератор или сохраните данные в список. - Путаница Iterable и Iterator — класс с
__iter__возвращающимselfи__next__одноразовый. Для многоразового обхода возвращайте новый итератор или используйтеyield. - Списковое включение вместо генераторного выражения —
[x for x in huge_data]съест всю память;(x for x in huge_data)— нет. - «Генератор списка» — разговорное название list comprehension. Это не генератор в смысле
yield/(...). - List comprehension ради побочных эффектов —
[print(x) for x in items]работает, но читается как ошибка. Используйте обычныйfor. - Отсутствие
withпри работе с файлами и ресурсами — «висящие» дескрипторы и утечки.
# Плохо: думаем, что пройдём дважды
gen = (x for x in range(3))
print(list(gen)) # [0, 1, 2]
print(list(gen)) # [] — генератор исчерпан
# Хорошо: новый генератор или список
print(list(x for x in range(3))) # [0, 1, 2]
Куда продолжить
- Про условия и циклы, которые питают итерации: Управляющие конструкции
- Про коллекции как источники и приёмники данных: Коллекции
- Про производительность и внутреннее выполнение: Архитектура выполнения и сборка мусора