Python range: ленивый диапазон, шаг и типичные ошибки

Программирование 27 сентябрь 2026 admin

Python range: ленивый диапазон, шаг и типичные ошибки

range в Python — одна из тех конструкций, которые знают все, но понимают немногие. Начинающие уверены, что это функция, возвращающая список чисел, и пишут list(range(n)) там, где список вообще не нужен. На практике range — это неизменяемая последовательность с ленивыми вычислениями: миллиард чисел в ней занимает 48 байт памяти. Такой фокус невозможен ни для списка, ни для массива numpy. Понимание трёх аргументов, полуинтервала [start, stop) и арифметики шага убирает целый класс ошибок: от вечных IndexError при обходе в обратную сторону до неожиданных TypeError при попытке задать дробный шаг. Ниже — разбор поведения range на уровне языка, честное сравнение с list и numpy.arange, замеры памяти и набор приёмов, которые реально используются в продакшн-коде.

Что такое range и почему это не список

В Python 3 range — это встроенный тип, а не функция. Вызов range(10) не создаёт коллекцию чисел, а конструирует объект, который хранит три значения: start, stop и step. Элементы вычисляются на лету при обращении. Именно поэтому range(10**9) создаётся мгновенно и не съедает гигабайты оперативной памяти — в отличие от Python 2, где range возвращал настоящий список, а для экономии приходилось брать xrange.

Читайте также: Android SDK Manager: установка, команды и типичные ошибки.

Объект range ведёт себя как последовательность: поддерживает len(), индексацию, отрицательные индексы, срезы и оператор in. Но он неизменяем — присвоить r[0] = 5 нельзя, и это логично: диапазон описывается математически, а не хранится поэлементно.

r = range(0, 10, 2)
print(r)          # range(0, 10, 2) — сам объект, а не список
print(list(r))    # [0, 2, 4, 6, 8]
print(len(r))     # 5
print(r[3])       # 6
print(6 in r)     # True
print(r[-1])      # 8

Отдельная деталь: range нельзя склеивать оператором +. Выражение range(3) + range(3) выбросит TypeError, потому что тип не реализует конкатенацию. Нужен itertools.chain или объединение через list, если порядок и повторяемость важны.

Три аргумента: start, stop, step

Сигнатура range допускает три формы вызова, и путаница между ними — источник половины ошибок новичков:

  • range(stop) — от 0 до stop, не включая stop;
  • range(start, stop) — от start до stop, не включая stop;
  • range(start, stop, step) — с заданным шагом, который может быть отрицательным.

Главное правило: правая граница всегда не входит в диапазон. Это полуинтервал [start, stop), и он выбран не случайно: количество элементов равно (stop - start + step - 1) // step при положительном шаге, а len(range(a, b)) = b - a. Никаких плюс-минут один при подсчёте длины.

print(list(range(5)))         # [0, 1, 2, 3, 4]
print(list(range(2, 5)))      # [2, 3, 4]
print(list(range(0, 10, 3)))  # [0, 3, 6, 9]
print(list(range(5, 0, -2)))  # [5, 3, 1]

# Шаг не может быть нулём — бесконечный цикл невозможен по определению
range(1, 10, 0)               # ValueError: range() arg 3 must not be zero
Если шаг положительный, а start больше stop — диапазон пуст. Если шаг отрицательный, а start меньше stop — тоже пуст. range молча вернёт ноль элементов, никакой ошибки не будет. Проверяйте знак шага, а не только границы.

Сравнение range, list и numpy.arange

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

Критерийrangelist(range(n))numpy.arange
Тип объектанеизменяемая последовательностьсписок Pythonndarray
Память при 106 элементах~48 байт~8 МБ только под указатели~8 МБ (int64) или ~4 МБ (int32)
Дробный шагнет (TypeError)нетда, но накапливает погрешность
Проверка вхождения x in objO(1), арифметикаO(n), линейный поискне применимо, вернёт массив булевых
Срезывозвращают rangeвозвращают новый списоквозвращают view
Векторные операциинетнетда
Типичное применениециклы, индексацияматериализация данныхнаучные расчёты, графики

Ключевой вывод: list(range(...)) нужен только тогда, когда вы собираетесь менять элементы, сортировать, добавлять или передавать коллекцию туда, где требуется именно список. Для обхода цикла материализация — чистая потеря памяти и времени.

Отрицательный шаг, срезы и проверка вхождения

Обратный проход — самый частый сценарий использования отрицательного шага. Здесь важно помнить: stop тоже не включается, поэтому для прохода до нуля включительно пишут range(n, -1, -1), а не range(n, 0, -1).

n = 5
print(list(range(n, 0, -1)))   # [5, 4, 3, 2, 1]
print(list(range(n, -1, -1)))  # [5, 4, 3, 2, 1, 0] — ноль включён
print(list(range(10)[::-1]))   # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

# Срез диапазона остаётся диапазоном, а не списком
print(range(0, 100, 5)[2:5])   # range(10, 25, 5)

Проверка вхождения в range работает за константное время — Python не перебирает элементы, а решает сравнение по модулю. Это редкая возможность для последовательности: у списка такая проверка линейна.

huge = range(0, 10**12, 3)
print(9 in huge)      # True  — мгновенно, несмотря на триллион элементов
print(10 in huge)     # False
range — один из немногих объектов, который умеет отвечать на вопрос о вхождении элемента без итерации. Если вам нужна быстрая проверка на принадлежность арифметической прогрессии, range даст её даром.

Память и скорость: сколько это стоит

Разницу удобно измерить через sys.getsizeof. Размер объекта range не зависит от длины диапазона — он определяется только тремя хранимыми числами и служебным заголовком.

import sys

r = range(10**9)
print(sys.getsizeof(r))          # 48 байт

small = range(10)
print(sys.getsizeof(small))      # те же 48 байт

# Список такого же размера физически не влезет в память
# list(range(10**9)) — это примерно 8 ГБ только под указатели

По скорости обхода range и список близки: итерация всё равно распаковывает целые числа. Но создание диапазона — операция O(1), тогда как list(range(n)) требует O(n) времени и памяти. В коде, который вызывается миллион раз, эта разница складывается в заметные секунды.

Личное наблюдение автора: пару лет назад я разбирал чужой скрипт обработки логов, который стабильно падал по OOM на файле в 400 МБ. Причина оказалась смешной — в одной строке стояло list(range(len(chunk))) для простого перебора индексов. Замена на range без материализации убрала падение целиком, а профилировщик показал экономию почти 300 МБ. С тех пор я считаю list(range(...)) без последующих мутаций списка красным флагом на код-ревью.

Практические приёмы и подводные камни

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

  • enumerate вместо range(len()) — короче, читаемее и работает с любым итерируемым объектом, включая генераторы;
  • zip для параллельного обхода — не нужно синхронизировать индексы вручную;
  • reversed(range(n)) работает и не создаёт копию, но reversed(list) уже требует поддержки __reversed__ или __len__;
  • дробный шаг — range его не умеет, и это осознанное решение. Для неравномерных шагов берите numpy.linspace или генератор;
  • range не итератор — его можно обходить многократно, в отличие от генератора, который исчерпывается после первого прохода.
items = [11, 22, 33]

# Плохо: индекс ради индекса
for i in range(len(items)):
    print(i, items[i])

# Хорошо
for i, item in enumerate(items, start=1):
    print(i, item)   # 1 11, 2 22, 3 33

# Параллельный обход
names = [a, b, c]
for name, value in zip(names, items):
    print(name, value)

Частые вопросы

Почему stop не включается? Полуинтервал даёт чистую арифметику длины и стыкуется без наложений: range(0, 5) и range(5, 10) идеально продолжают друг друга.

range(0.1, 1, 0.1) сработает? Нет, TypeError. Для дробных значений используйте numpy.arange с осторожностью (накапливается погрешность) или list comprehension с Decimal.

Можно ли менять диапазон во время цикла? Объект неизменяем, но саму переменную можно переприсвоить — цикл при этом продолжит идти по исходному объекту, потому что итератор уже создан.

range(5) == range(0, 5)? Да, диапазоны сравниваются как последовательности, а не по идентичности объекта.

Коротко от автора

range — пример удачного дизайна стандартной библиотеки: минимум сущностей, предсказуемое поведение и нулевая цена по памяти. Освойте три аргумента, держите в голове полуинтервал и не материализуйте диапазон без нужды — и код станет заметно опрятнее. Если после прочтения вы поймали себя на мысли, что где-то в вашем проекте есть лишний list(range(...)), значит статья отработала свою пользу.

Редакция «ИТ Фишки»
Качественные материалы о софте, технологиях и гаджетах
Ссылка на материал
Поделиться:
Похожие публикации
Комментарии (0)

© Copyright © 2026. Все права защищены.