Класс range в Python: диапазоны, шаг и применение
Обновлено:
В Python существует класс:
range
Он используется для представления последовательности целых чисел.
Чаще всего range встречается вместе с циклом:
for
Например:
for number in range(10):
print(number)
Результат:
0
1
2
3
4
5
6
7
8
9
Значение:
10
не включается.
1. range — это класс
Проверим:
numbers = range(10)
print(type(numbers))
Результат:
<class 'range'>
Объект range является:
- итерируемым объектом;
- неизменяемой последовательностью;
- объектом, поддерживающим индексы и срезы.
2. Самый простой range
Если передать один аргумент:
range(10)
он воспринимается как:
stop = 10
При этом начало по умолчанию:
0
а шаг:
1
То есть:
list(
range(10)
)
даст:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Можно условно представить:
start → 0
stop → 10
step → 1
3. Преобразование range
Чтобы сразу увидеть все значения диапазона, его удобно временно преобразовать в список:
numbers = list(
range(10)
)
print(numbers)
Результат:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Можно создать и кортеж:
numbers = tuple(
range(5)
)
print(numbers)
Результат:
(0, 1, 2, 3, 4)
Сам объект range при этом остаётся отдельным типом данных.
start и stop
4. Указываем начало
Если передать два аргумента:
range(start, stop)
первый является началом диапазона.
print(
list(
range(5, 10)
)
)
Результат:
[5, 6, 7, 8, 9]
Здесь:
start → 5
stop → 10
step → 1
start включается, а stop — нет.
5. Отрицательные числа
print(
list(
range(-5, 5)
)
)
Результат:
[-5, -4, -3, -2, -1, 0, 1, 2, 3, 4]
Шаг
6. range(start, stop, step)
Третий аргумент определяет шаг:
range(start, stop, step)
Например:
print(
list(
range(0, 10, 2)
)
)
Результат:
[0, 2, 4, 6, 8]
Здесь:
start → 0
stop → 10
step → 2
7. Другой шаг
print(
list(
range(5, 30, 5)
)
)
Результат:
[5, 10, 15, 20, 25]
Отрицательный шаг
8. Движение справа налево
print(
list(
range(10, 0, -1)
)
)
Результат:
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
Чтобы двигаться в сторону уменьшения, шаг должен быть отрицательным.
9. Отрицательный шаг больше единицы
print(
list(
range(10, 0, -2)
)
)
Результат:
[10, 8, 6, 4, 2]
Направление должно соответствовать шагу
10. Пустой range
Например:
print(
list(
range(10, 0)
)
)
Результат:
[]
По умолчанию:
step = 1
то есть значения должны увеличиваться.
Но начать мы пытаемся с 10, а остановиться около 0.
Такой диапазон невозможен при положительном шаге.
Нужно:
range(10, 0, -1)
А:
range(0, 10, -1)
тоже даст пустую последовательность.
Нулевой шаг
11. step не может быть равен нулю
range(0, 10, 0)
приведёт к:
ValueError
Потому что при шаге 0 невозможно двигаться к окончанию диапазона.
Используем шаг вместо лишних проверок
12. Числа, кратные пяти
Предположим, нужно вывести числа, кратные 5.
Можно написать:
for number in range(100):
if number % 5 == 0:
print(number)
Такой алгоритм работает.
Но цикл должен:
- сделать 100 итераций;
- 100 раз вычислить остаток от деления;
- 100 раз выполнить условие.
Гораздо лучше сразу задать нужный шаг:
for number in range(0, 100, 5):
print(number)
Цикл получит только:
0
5
10
15
20
...
95
Количество итераций:
20
Дополнительное условие:
if
вообще не требуется.
13. Используем возможности range
Если необходимые значения можно выразить через:
start
stop
step
лучше сразу сформировать правильный диапазон, чем обходить лишние числа и фильтровать их внутри цикла.
Это делает код:
- проще;
- понятнее;
- эффективнее.
range как счётчик
14. Повторить действие определённое количество раз
Иногда само число из range нам вообще не нужно.
Например, хотим пять раз вывести сообщение:
for _ in range(5):
print('Hello')
Результат:
Hello
Hello
Hello
Hello
Hello
Здесь:
_
показывает:
Значение переменной нам не интересно. Нам важно только количество итераций.
range(5) создаёт возможность выполнить тело цикла пять раз.
Почему range не стоит заменять огромным списком
15. range не хранит все числа диапазона
Представим:
numbers = range(
1_000_000_000
)
Это не означает, что Python создал миллиард объектов int и сохранил миллиард ссылок на них.
range хранит информацию, необходимую для вычисления последовательности:
start
stop
step
В реализации CPython также хранится вычисленная длина диапазона.
Но все элементы диапазона заранее в памяти не создаются.
16. Значения вычисляются по необходимости
Когда цикл просит очередное значение:
for number in range(1_000_000_000):
...
range вычисляет очередное число по мере обхода.
Такой подход часто называют ленивым вычислением значений.
Упрощённо:
range
↓
хранит параметры диапазона
↓
цикл просит следующее значение
↓
range вычисляет его
↓
цикл просит следующее
↓
range вычисляет следующее
Память
17. Огромный range остаётся небольшим объектом
Например:
from sys import getsizeof
small_range = range(10)
large_range = range(
1_000_000_000
)
print(
getsizeof(small_range)
)
print(
getsizeof(large_range)
)
В CPython размеры этих объектов обычно будут одинаковыми или близкими.
Точное количество байтов зависит от реализации и версии Python, поэтому конкретное число запоминать не нужно.
Главная идея:
Размер
rangeпрактически не зависит от количества чисел, которые он описывает.
18. Но преобразование в список меняет ситуацию
numbers = list(
range(1_000_000)
)
Теперь действительно создаётся список, который должен хранить ссылки на множество объектов.
Поэтому:
range(1_000_000)
и:
list(
range(1_000_000)
)
с точки зрения памяти — совершенно разные вещи.
len()
19. Длина диапазона
Так как range является последовательностью:
numbers = range(
10,
100,
10,
)
print(
len(numbers)
)
Результат:
9
Диапазон:
10
20
30
40
50
60
70
80
90
содержит девять значений.
Для получения длины Python не требуется сначала создавать все эти числа.
Другие функции
20. sum()
range является итерируемым объектом, поэтому его можно передать в:
sum()
Например:
result = sum(
range(1, 6)
)
print(result)
Получим:
15
Потому что:
1 + 2 + 3 + 4 + 5 = 15
21. min() и max()
numbers = range(
10,
100,
10,
)
print(
min(numbers)
)
print(
max(numbers)
)
Результат:
10
90
Индексы
22. range поддерживает индексацию
numbers = range(
10,
100,
10,
)
print(
numbers[0]
)
print(
numbers[3]
)
Результат:
10
40
Работают и отрицательные индексы:
print(
numbers[-1]
)
Получим:
90
Срезы
23. Можно получить срез range
numbers = range(10)
result = numbers[2:8:2]
print(result)
Получим новый объект:
range(2, 8, 2)
Если преобразовать:
print(
list(result)
)
получим:
[2, 4, 6]
То есть срез range возвращает не список, а новый объект range.
in
24. Проверка наличия значения
numbers = range(
0,
100,
5,
)
print(
25 in numbers
)
print(
26 in numbers
)
Результат:
True
False
range умеет математически определить, принадлежит ли число его последовательности, и ему не обязательно последовательно обходить весь диапазон.
Методы count() и index()
25. count()
Как и другие изученные последовательности, range имеет:
count()
Например:
numbers = range(
0,
100,
5,
)
print(
numbers.count(25)
)
print(
numbers.count(26)
)
Результат:
1
0
Значения внутри одного range не повторяются, поэтому результат фактически будет:
0
или:
1
26. index()
numbers = range(
10,
100,
10,
)
print(
numbers.index(40)
)
Результат:
3
Если значения нет:
numbers.index(45)
возникнет:
ValueError
Обход последовательности по индексам
27. range(len(...))
До этого мы обходили список напрямую:
fruits = [
'apple',
'banana',
'orange',
]
for fruit in fruits:
print(fruit)
Если нам нужен именно индекс элемента, можно использовать:
for index in range(
len(fruits)
):
print(
index,
fruits[index],
)
Результат:
0 apple
1 banana
2 orange
Здесь:
len(fruits)
равен:
3
поэтому:
range(3)
даёт:
0
1
2
То есть именно существующие индексы списка.
Обход в обратном порядке
28. Индексы справа налево
fruits = [
'apple',
'banana',
'orange',
]
for index in range(
len(fruits) - 1,
-1,
-1,
):
print(
fruits[index]
)
Результат:
orange
banana
apple
Для списка длиной 3:
start → 2
stop → -1
step → -1
Получаем индексы:
2
1
0
Соседние элементы
29. Берём сразу два элемента
Предположим:
numbers = [
10,
20,
30,
40,
50,
]
Можно одновременно получать текущий и следующий элемент:
for index in range(
len(numbers) - 1
):
print(
numbers[index],
numbers[index + 1],
)
Результат:
10 20
20 30
30 40
40 50
30. Почему len(numbers) - 1
Внутри мы обращаемся:
numbers[index + 1]
Поэтому последний возможный index должен быть предпоследним индексом списка.
Иначе в конце мы попытались бы обратиться за пределы списка:
numbers[len(numbers)]
и получили бы:
IndexError
Окна с перекрытием
31. Соседние пары пересекаются
Мы получили:
10 20
20 30
30 40
40 50
Обратите внимание:
20
30
40
используются дважды.
Например:
10 20
20 30
30 40
Такой обход можно представить как движущееся окно из двух элементов.
32. Три соседних элемента
numbers = [
10,
20,
30,
40,
50,
]
for index in range(
len(numbers) - 2
):
print(
numbers[index],
numbers[index + 1],
numbers[index + 2],
)
Результат:
10 20 30
20 30 40
30 40 50
Без перекрытия
33. Берём элементы парами
Если нужно:
10 20
30 40
50 60
без перекрытия, можно использовать шаг 2.
numbers = [
10,
20,
30,
40,
50,
60,
]
for index in range(
0,
len(numbers) - 1,
2,
):
print(
numbers[index],
numbers[index + 1],
)
Результат:
10 20
30 40
50 60
Индексы:
0
2
4
Факториал
34. Что такое факториал
Факториал неотрицательного целого числа n — произведение всех целых чисел от 1 до n.
Например:
5! = 1 × 2 × 3 × 4 × 5
Получаем:
120
Также:
0! = 1
1! = 1
35. Функция для вычисления факториала
Предполагаем, что функция получает неотрицательное целое число.
def factorial(number):
result = 1
for value in range(
2,
number + 1,
):
result *= value
return result
Используем:
print(
factorial(5)
)
Результат:
120
36. Как работает алгоритм
Начинаем:
result = 1
range:
range(2, 6)
даёт:
2
3
4
5
Первая итерация:
result = 1 × 2 = 2
Вторая:
result = 2 × 3 = 6
Третья:
result = 6 × 4 = 24
Четвёртая:
result = 24 × 5 = 120
Возвращаем:
120
37. Почему начинаем с 1
Если написать:
result = 0
то любое умножение:
0 × число
останется:
0
Поэтому накопитель произведения начинается с:
1
38. Почему начинаем range с 2
Умножение на:
1
ничего не изменяет.
Поэтому вместо:
range(1, number + 1)
можно сразу написать:
range(2, number + 1)
Также благодаря этому:
factorial(0)
и:
factorial(1)
получат пустой range, цикл не выполнится ни разу, а функция вернёт исходный:
1
что является правильным результатом.
Числа Фибоначчи
39. Последовательность Fibonacci
Последовательность начинается:
0
1
1
2
3
5
8
13
21
34
...
Каждое следующее число получается сложением двух предыдущих:
0 + 1 = 1
1 + 1 = 2
1 + 2 = 3
2 + 3 = 5
3 + 5 = 8
Будем считать:
F₀ = 0
F₁ = 1
Нахождение числа Fibonacci
40. Функция
Предполагаем, что number — неотрицательное целое число.
def fibonacci(number):
first = 0
second = 1
for _ in range(number):
first, second = (
second,
first + second,
)
return first
Например:
print(
fibonacci(7)
)
Результат:
13
Потому что:
F₀ = 0
F₁ = 1
F₂ = 1
F₃ = 2
F₄ = 3
F₅ = 5
F₆ = 8
F₇ = 13
41. Как работает функция
Изначально:
first = 0
second = 1
Первая итерация:
first, second = (
second,
first + second,
)
Справа сначала вычисляются значения:
second → 1
first+second → 1
Получаем:
first = 1
second = 1
Следующая итерация:
first = 1
second = 2
Затем:
first = 2
second = 3
Дальше:
first = 3
second = 5
first = 5
second = 8
first = 8
second = 13
first = 13
second = 21
После нужного количества итераций:
return first
возвращает искомое число.
42. Почему используется _
for _ in range(number):
нам не важно само значение:
0
1
2
3
...
Нужно лишь выполнить действие:
number раз
Поэтому используется ненужная переменная:
_
Диапазон или математическая формула
43. Сумма чисел от 1 до n
Предположим, необходимо найти:
1 + 2 + 3 + ... + n
Можно использовать:
sum(
range(1, n + 1)
)
Например:
def sum_range(number):
return sum(
range(
1,
number + 1,
)
)
Алгоритму приходится обработать все числа последовательности.
При:
n = 250 000
нужно обработать:
250 000 значений
Поэтому сложность относительно количества значений:
O(n)
Формула арифметической прогрессии
44. Сумма от 1 до n
Для этой конкретной последовательности существует формула:
n × (n + 1)
───────────
2
В Python:
def sum_numbers(number):
return (
number
* (number + 1)
// 2
)
Например:
print(
sum_numbers(100)
)
Получим:
5050
Сравнение алгоритмов
45. timeit
from timeit import timeit
number = 250_000
def sum_range(number):
return sum(
range(
number + 1
)
)
def sum_numbers(number):
return (
number
* (number + 1)
// 2
)
print(
timeit(
lambda: sum_range(number),
number=250,
)
)
print(
timeit(
lambda: sum_numbers(number),
number=250,
)
)
Разница будет очень большой.
Первый алгоритм:
sum(
range(
number + 1
)
)
должен обработать все значения диапазона:
0
1
2
3
...
250000
То есть количество работы растёт вместе с number.
Упрощённо:
O(n)
Второй:
number * (number + 1) // 2
выполняет фиксированное количество арифметических операций.
В обычном вводном анализе такой алгоритм рассматривают как:
O(1)
относительно количества чисел последовательности.
Важный вывод об алгоритмах
Один и тот же результат иногда можно получить совершенно разными способами.
Например:
перебрать 250 000 чисел
или:
выполнить несколько математических операций
Оба алгоритма дают правильный ответ.
Но их производительность отличается кардинально.
Поэтому при разработке важно не только написать работающий код, но и подумать:
Обязательно ли вообще перебирать все элементы?
Иногда известная математическая формула позволяет полностью убрать цикл.
Кратко о range
Основные формы:
range(stop)
range(start, stop)
range(start, stop, step)
Например:
list(
range(5)
)
[0, 1, 2, 3, 4]
list(
range(5, 10)
)
[5, 6, 7, 8, 9]
list(
range(0, 10, 2)
)
[0, 2, 4, 6, 8]
list(
range(10, 0, -2)
)
[10, 8, 6, 4, 2]
Главное по теме
range — класс, представляющий неизменяемую последовательность целых чисел.
Он является итерируемым объектом:
for number in range(10):
...
Поддерживает:
start
stop
step
stop не включается.
range не создаёт заранее все описываемые им числа, поэтому очень экономно использует оперативную память.
range(
1_000_000_000
)
не равнозначен созданию:
list(
range(
1_000_000_000
)
)
Если нужную последовательность можно сразу задать шагом:
range(0, 100, 5)
лучше не делать лишние итерации и проверки:
for number in range(100):
if number % 5 == 0:
...
Если значение счётчика вообще не используется:
for _ in range(5):
...
range удобно применять просто для нужного количества повторений.
Для обхода последовательности по индексам:
for index in range(
len(values)
):
...
Для соседних элементов:
for index in range(
len(values) - 1
):
first = values[index]
second = values[index + 1]
И главное:
Цикл нужен только тогда, когда задачу действительно необходимо решать перебором.
Если существует прямой математический алгоритм, он может оказаться на порядки эффективнее.