Привет, Хабр!
Сегодня коротко, но по существу разберёмся, зачем вообще нужен enumerate() в Python и почему он почти всегда лучше, чем старый добрый range(len(...)).
Проблема range(len(...))
Сразу:
records = fetch_records() for i in range(len(records)): process(records[i])
С виду — ничего страшного. Даже читается вроде знакомо. Но чем больше таких конструкций в кодовой базе, тем хуже.
Читаемость
Такой цикл требует ментального дешифрования:
i— это что? индекс? порядковый номер? offset?records[i]— где сам элемент? Где структура?process(records[i])— приходится сканировать глазами вверх, чтобы вспомнить, откудаrecords, и чтоi— к нему.
Это не очевидный код. Читатель должен это угадывать — каждый раз. Учитывая, что Python — динамический язык, мы вообще не знаем, что такое records.
Хрупкость: генераторы, ленивые структуры, итераторы
А что будет, если records — это не список, а генератор? Например:
records = (record for record in db_stream())
len(records) не сработает вовсе: TypeError: object of type 'generator' has no len(). Даже если обернёте list(...), потеряете ленивость, получите RAM-overflow на больших входных.
Другими словами:
range(len(...))— это допущение, что ваш объект точно индексируем и точно заранее измерим. Это ложная уверенность.
Баги
Пример:
for i in range(len(records)): if should_skip(records[i]): continue process(records[i])
Теперь представим, что кто-то хочет отложить records[i] в переменную:
for i in range(len(records)): if should_skip(records[i]): continue rec = records[i] process(rec)
Но уже два обращения по индексу. А если вставим удаление?
for i in range(len(records)): if should_skip(records[i]): records.pop(i)
Ломаем индексы на ходу, т.к удаление смещает следующую итерацию, и цикл перепрыгивает элементы.
Почти всегда это приводит к одному из двух:
IndexError(если удалили последний элемент),или тихой логической ошибке, которую потом дебажим неделю, обвиняя кеш, БД и прокси.
Стандарт нарушается
Python считается языком, где for element in iterable — это норма. Когда вы пишете for i in range(len(...)), вы нарушаете естественный паттерн языка.
Это похоже на то, как если бы в Rust кто-то вручную писал for i in 0..vec.len() вместо for (i, item) in vec.iter().enumerate() — да, можно, но сразу видно: человек ещё не прочувствовал стиль языка.
Как enumerate() делает код чище
Берём тот же цикл:
for i, record in enumerate(records): process(record)
С первого взгляда — минимальная правка. Но если копнуть, это переход от «примитивного императивного кода» к «декларативной конструкции, встроенной в семантику языка».
Читаемость:
enumerate() говорит на человеческом языке:
for index, value in enumerate(collection):
— ты читаешь: "Итерируйся по collection, на каждом шаге у тебя будет index и value". Без len, без collection[index], без зрительного слома, где что. Код документирует сам себя.
Безопасность
В отличие от range(len(...)), enumerate():
не зависит от индексируемости: можно пройтись по генератору, файлу, сокету, пайплайну;
не требует
len(): не вызывается ничего, что может сломаться на ленивых структурах;не дублирует обращение к коллекции:
records[i]— больше не нужно.
records = get_lazy_stream() for idx, rec in enumerate(records): process(rec) # здесь всё живёт
А если бы get_lazy_stream() возвращал генератор, range(len(...)) выдал бы TypeError.
Поведение
Поглядим на enumerate():
>>> e = enumerate(['🍏', '🍐', '🍊']) >>> e <enumerate object at 0x...> >>> next(e) (0, '🍏') >>> next(e) (1, '🍐')
В CPython, enumerate() реализован в bltinmodule.c как builtin_enumerate, и внутри просто инкрементирует счётчик, вызывая PyIter_Next для источника.
Формально:
static PyObject * enumerate_next(enumerateobject *en) { PyObject *next_item = PyIter_Next(en->it); ... result = PyTuple_Pack(2, en->index, next_item); en->index++; }
Это максимально дешёвый итератор. Он:
не держит копии,
не вызывает
len(),не требует seekable-источника.
enumerate() умеет начинать с любого значения
for i, el in enumerate(seq, start=1): print(f"{i}: {el}")
Аргумент start полезен в пользовательских интерфейсах (CLI, логах, отчётах), где 1-based нумерация привычнее, чем 0.
Применение
Классический цикл с логированием
for idx, user in enumerate(users): if idx % 1000 == 0: logger.info("Обработано %d пользователей", idx) enrich(user)
Без ручного счётчика.
Объединение с zip — параллельная обработка
for idx, (x, y) in enumerate(zip(xs, ys)): result.append(x + y)
Работает даже если xs, ys — это генераторы. range(len(...)) бы тут не сработал: у zip нет длины.
Чтение из файла + отслеживание строк
with open("config.ini") as f: for lineno, line in enumerate(f, start=1): if '=' not in line: raise ValueError(f"Ошибка на строке {lineno}: {line.strip()}")
lineno есть, line есть. Никаких .readlines() (которые грузят всё в память), никакого range.
Быстрая индексация в генераторе
indexed = {i: val for i, val in enumerate(seq) if val}
Однострочник, который всё делает правильно: итерируется, фильтрует и создаёт map.
Мутация по индексам (safe)
for idx, el in enumerate(items): if is_corrupted(el): items[idx] = repair(el)
В отличие от удаления (где всё ломается), замена значения по индексу через enumerate() — безопасна.
Асинхронная версия — aenumerate() (в trio)
async for idx, event in aenumerate(event_stream(), start=42): await process_event(idx, event)
Работает ровно как sync-версия. А если нет aenumerate, можно руками:
async def aenumerate(aiter, start=0): idx = start async for val in aiter: yield idx, val idx += 1
Заключение
enumerate() — это один из тех инструментов, которые вроде бы мелочь, но реально спасают от бардака: избавляют от лишней логики, не ломаются на генераторах, и читаются легко. А если у вас есть интересный опыт — делитесь в комментариях, обсудим.
Если настройки окружения и постоянные ошибки вам знакомы, то этот открытый урок 24 июля — именно то, что нужно. Узнайте, как Docker упрощает деплой и разработку Python-приложений. Мы разберем:
Что такое Docker и зачем он нужен.
Как контейнеры упрощают работу.
Как создавать и использовать Docker-образы для Python.
Присоединяйтесь, чтобы понять, как профессионалы решают проблемы окружений и деплоя.
Пройдите вступительный тест курса "Python Developer. Basic" и получите скидку на обучение.
