Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Итератор vs Генератор в Python
Отличный базовый вопрос про Python, который очень актуален для Data Engineer, работающего с большими объёмами данных. Разберу различия.
Основные определения
Итератор — это объект, который имеет методы __iter__() и __next__():
__iter__()— возвращает сам итератор__next__()— возвращает следующий элемент
Генератор — это функция, которая использует yield для возврата значений по одному. Генератор — это тип итератора (но не наоборот).
# Генератор — это частный случай итератора
Генератор ⊂ Итератор
Пример 1: Итератор (класс)
# Создаём итератор вручную
class CountUp:
def __init__(self, max):
self.max = max
self.current = 0
def __iter__(self): # Метод iter
return self
def __next__(self): # Метод next
if self.current < self.max:
self.current += 1
return self.current
else:
raise StopIteration # Обязательно для завершения
# Использование
count = CountUp(3)
for num in count:
print(num) # 1, 2, 3
# Или вручную
count = CountUp(3)
print(next(count)) # 1
print(next(count)) # 2
print(next(count)) # 3
print(next(count)) # StopIteration Exception
Пример 2: Генератор (функция с yield)
# То же самое, но генератором (проще!)
def count_up(max):
current = 0
while current < max:
current += 1
yield current # Возвращаем значение и паузируем
# Использование (идентично итератору выше)
for num in count_up(3):
print(num) # 1, 2, 3
# Или вручную
gen = count_up(3)
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # 3
print(next(gen)) # StopIteration (автоматично!)
Сравнительная таблица
| Аспект | Итератор | Генератор |
|---|---|---|
| Что это | Объект класса | Функция |
| Создание | class MyIter: __iter__(), __next__() | def my_gen(): yield |
| Методы | Нужно определить __iter__() и __next__() | Автоматические |
| Состояние | В переменных объекта (self.x) | В локальных переменных функции |
| Код | Больше кода (~10-15 строк) | Меньше кода (~3-5 строк) |
| Память | Держит состояние в объекте | Более эффективно |
| Производительность | Быстрее немного | Чуть медленнее (функция) |
Пример 3: Практический кейс для Data Engineer
Задача: Читаем CSV файл с 1 миллиардом строк. Не можем загрузить всё в память.
Плохо (без итератора):
# Это упадёт с MemoryError!
import pandas as pd
df = pd.read_csv('huge_file.csv') # Пытаемся загрузить 1 млрд строк
# MemoryError: Unable to allocate 500 GB for data
Хорошо (с итератором):
# Способ 1: Используем read_csv с chunksize (это итератор!)
import pandas as pd
for chunk in pd.read_csv('huge_file.csv', chunksize=10000):
# Обрабатываем по 10K строк за раз
print(f"Processing {len(chunk)} rows")
# chunk — это DataFrame с 10K строк
# После loop — старый chunk удаляется из памяти
print("Done! Обработали все данные без OutOfMemory")
Хорошо (с генератором):
# Способ 2: Пишем свой генератор
def read_large_csv(filename, chunksize=10000):
with open(filename) as f:
chunk = []
for line in f:
chunk.append(line.strip().split(','))
if len(chunk) == chunksize:
yield chunk # Отправляем 10K строк
chunk = [] # Очищаем память
if chunk:
yield chunk # Последний partial chunk
# Использование
for batch in read_large_csv('huge_file.csv', chunksize=10000):
print(f"Processing batch of {len(batch)} rows")
# Обрабатываем batch
for row in batch:
# process row
pass
Пример 4: Внутренний механизм yield
Что происходит при yield:
def my_gen():
print("Start") # Выполняется при next() #1
yield 1 # Возвращаем 1, паузируем здесь
print("Middle") # Выполняется при next() #2
yield 2 # Возвращаем 2, паузируем здесь
print("End") # Выполняется при next() #3
yield 3 # Возвращаем 3, паузируем здесь
gen = my_gen()
print("Created generator")
print(next(gen)) # Output: "Start" → returns 1
print(next(gen)) # Output: "Middle" → returns 2
print(next(gen)) # Output: "End" → returns 3
print(next(gen)) # StopIteration (функция закончена)
Вывод:
Created generator
Start
1
Middle
2
End
3
Ключевой момент: код в функции выполняется пошагово, не весь сразу!
Пример 5: Генератор выражений
Есть ещё более лёгкий способ — generator expression:
# Обычный список (съедает память)
numbers = [x for x in range(1000000)] # Создаёт список в памяти, ~40 MB
# Generator expression (не съедает память)
numbers_gen = (x for x in range(1000000)) # Скобки вместо квадратных!
# Разница:
print(numbers[999999]) # Быстро (уже в памяти)
next(numbers_gen) # Ленивое вычисление
# Generator expression занимает ~0 MB до первого использования
Пример 6: Цепочка генераторов
Для Data Engineering часто нужно цепочка обработок:
def read_csv(filename):
"""Читаем CSV построчно"""
with open(filename) as f:
for line in f:
yield line.strip().split(',')
def filter_valid(rows):
"""Фильтруем невалидные строки"""
for row in rows:
if len(row) == 4: # Ожидаем 4 колонки
yield row
def parse_numbers(rows):
"""Преобразуем строки в числа"""
for row in rows:
try:
yield [row[0], int(row[1]), float(row[2]), row[3]]
except ValueError:
pass # Пропускаем строки с ошибками
def transform_to_dict(rows):
"""Преобразуем в словари"""
for row in rows:
yield {
'id': row[0],
'qty': row[1],
'price': row[2],
'name': row[3]
}
# Pipeline
data = read_csv('data.csv')
data = filter_valid(data)
data = parse_numbers(data)
data = transform_to_dict(data)
# Обработка (всё выполняется лениво!)
for item in data:
print(f"Processing: {item['name']} (qty={item['qty']})")
# Память: только 1 строка в памяти в момент времени!
Пример 7: Бесконечный генератор
Генераторы хороши для бесконечных последовательностей:
# Бесконечный счётчик
def infinite_counter(start=0):
n = start
while True:
yield n
n += 1
# Использование (берём первые 5)
from itertools import islice
for num in islice(infinite_counter(100), 5):
print(num) # 100, 101, 102, 103, 104
# Итератор же требовал бы while True loop явно
Пример 8: Производительность итератор vs генератор
import time
# Итератор (класс)
class RangeIterator:
def __init__(self, n):
self.n = n
self.i = 0
def __iter__(self):
return self
def __next__(self):
if self.i < self.n:
self.i += 1
return self.i
raise StopIteration
# Генератор
def range_gen(n):
for i in range(n):
yield i
# Бенчмарк
n = 10_000_000
start = time.time()
for x in RangeIterator(n):
pass
print(f"Iterator: {time.time() - start:.2f}s")
start = time.time()
for x in range_gen(n):
pass
print(f"Generator: {time.time() - start:.2f}s")
start = time.time()
for x in range(n):
pass
print(f"Built-in range: {time.time() - start:.2f}s")
# Примерные результаты:
# Iterator: 0.82s
# Generator: 0.95s
# Built-in range: 0.75s
# (встроенные функции быстрее)
Когда использовать?
Генератор когда:
- ✅ Нужна простота (yield вместо next)
- ✅ Большой объём данных (memory efficient)
- ✅ Pipeline обработки данных
- ✅ Бесконечные последовательности
- ✅ Лёгкое преобразование
Итератор когда:
- ✅ Нужна контроль (сложная логика состояния)
- ✅ Несколько методов кроме next()
- ✅ Нужна производительность (чуть быстрее)
- ✅ Переиспользуемый класс
Best Practices для Data Engineer
- Используй генераторы по умолчанию — проще писать
- Для больших файлов — всегда chunked processing
- Pipeline с генераторами — очень efficient
- Профилируй память — убедись, что не растёт
itertools— твой друг — islice, chain, groupby и т.д.
from itertools import islice, chain, groupby
# Примеры
first_5 = islice(my_gen, 5) # Первые 5 элементов
chained = chain(gen1, gen2, gen3) # Объедини генераторы
by_key = groupby(sorted_data, key) # Группируй
Заключение
Главное различие:
- Итератор — это объект с методами
__iter__()и__next__() - Генератор — это функция с
yield, которая автоматически создаёт итератор
Для Data Engineer: генераторы — это твой основной инструмент для работы с большими данными. Они экономят память, упрощают код и делают его readable. Используй их везде, где обрабатываешь потоковые или большие данные.