Справочник по Python · модуль 3 · списки и циклы
Глава 3.6
Перебор списка
по индексам
for index in range(len(items)) перебирает не элементы, а их позиции. Такой подход нужен для изменения списка, сравнения соседей и алгоритмов, зависящих от индекса.
- Категория
- Списки и циклы
- Уровень
- Начальный
- Связанные темы
- len(), range(), for, индексы
- Зачем это знать
- Изменять элементы и обращаться к соседним позициям
§1Зачем нужны индексы
Обычный цикл for number in numbers выдаёт значения элементов и ничего не сообщает об их позициях. Пока задача сводится к чтению — вывести, посчитать сумму, найти максимум — этого достаточно.
Но некоторые задачи формулируются именно через позицию: «заменить каждый элемент на удвоенный», «сравнить элемент со следующим», «поменять местами первый и последний». Для них нужна не копия значения, а сам номер ячейки.
for number in numbers: → 10, 20, 30, 40 (значения) for index in range(4): → 0, 1, 2, 3 (позиции)
Позиция ценна тем, что по ней можно вернуться к списку и не только прочитать элемент, но и записать в него новое значение.
§2Разбор конструкции
Пусть numbers = [10, 20, 30, 40]. Выражение len(numbers) даёт 4, а range(4) — индексы 0, 1, 2, 3.
for index in range(len(numbers)): └ 4 └ range(4) → 0, 1, 2, 3 └ index по очереди получает каждую позицию
Выражение читается изнутри наружу.
len(numbers)вычисляется первым и даёт количество элементов —4.range(4)превращает количество в набор позиций0, 1, 2, 3.- Цикл
forперебирает этот набор и кладёт очередное число в переменнуюindex.
Число 4 пришлось бы исправлять вручную при каждом изменении списка. len(numbers) вычисляется при запуске и подходит любому списку, включая пустой: тогда range(0) пуст, и тело цикла не выполнится ни разу.
§3Индекс и элемент
index хранит позицию, а numbers[index] получает элемент на этой позиции. Это два разных значения, и путать их — самая частая ошибка в теме.
numbers = [10, 20, 30, 40] for index in range(len(numbers)): print(index, numbers[index])
0 10 1 20 2 30 3 40
В левом столбце — номера позиций, в правом — то, что в этих позициях лежит. Совпадать они не обязаны и обычно не совпадают.
§4Пошаговое чтение
| index | Выражение | Результат |
|---|---|---|
0 | numbers[0] | 10 |
1 | numbers[1] | 20 |
2 | numbers[2] | 30 |
3 | numbers[3] | 40 |
Здесь список только читается и потому не меняется. Дальше начинается запись, и таблицу придётся дополнить состоянием списка.
§5Изменение элемента
Главная строка всей главы выглядит так:
numbers[index] = numbers[index] * 2
Слева и справа стоит одна и та же запись numbers[index], но работает она по-разному. Python вычисляет присваивание справа налево.
- Сначала полностью вычисляется правая часть. Запись
numbers[index]справа означает «взять значение из ячейки»: приindex = 0получится1. - Полученное значение умножается на
2— получается2. Список в этот момент ещё не изменён. - Затем срабатывает левая часть. Запись
numbers[index]слева означает «положить значение в ячейку» — результат записывается в позицию0списка.
numbers[index] = numbers[index] * 2
│ │
│ └ ② прочитать значение из позиции
└ ③ записать результат в ту же позицию
Иначе говоря, квадратные скобки слева от знака = адресуют ячейку, а справа — достают её содержимое. Именно поэтому старое значение успевает поучаствовать в вычислении нового.
numbers = [1, 2, 3, 4] for index in range(len(numbers)): numbers[index] = numbers[index] * 2 print(numbers)
[2, 4, 6, 8]
§6Трассировка изменения
Чтобы увидеть механику целиком, полезно расписать все четыре повтора и состояние списка после каждого из них.
| index | Прочитано справа | Вычислено | Список после шага |
|---|---|---|---|
0 | 1 | 1 * 2 = 2 | [2, 2, 3, 4] |
1 | 2 | 2 * 2 = 4 | [2, 4, 3, 4] |
2 | 3 | 3 * 2 = 6 | [2, 4, 6, 4] |
3 | 4 | 4 * 2 = 8 | [2, 4, 6, 8] |
Список меняется постепенно, по одной ячейке за повтор. Новый список не создаётся: всё это время numbers указывает на одну и ту же коллекцию, у которой обновляется содержимое.
Строку можно записать короче: numbers[index] *= 2. Результат тот же, порядок действий тот же. Начинающим полезнее сначала освоить полную форму, чтобы видеть чтение и запись явно.
§7Почему обычный for не меняет список
Логично попробовать сделать то же самое без индексов. Такой код запускается, не выдаёт ошибок — и не работает.
numbers = [1, 2, 3, 4] for number in numbers: number = number * 2 print(numbers)
[1, 2, 3, 4]
Причина в том, что number — это отдельная переменная цикла, а не сама ячейка списка. На каждом повторе в неё копируется значение элемента.
Строка number = number * 2 присваивает новое значение имени number. Связь между этим именем и позицией 0 при этом теряется: переменная начинает указывать на число 2, а ячейка списка по-прежнему хранит 1.
На следующем повторе number просто перезаписывается значением следующего элемента, и вычисленная двойка теряется бесследно. Чтобы изменение попало в список, адресовать нужно ячейку — то есть писать numbers[index] = ....
Это опасный случай: программа отрабатывает молча, вывод выглядит правдоподобно, и заметить проблему можно только по значениям. Если после цикла список «почему-то не изменился» — первым делом проверьте, присваиваете вы в переменную или в позицию списка.
§8Соседние элементы
Вторая причина использовать индексы: имея позицию, можно обратиться и к соседним — index - 1 и index + 1.
numbers = [5, 8, 3, 10] for index in range(len(numbers) - 1): print(numbers[index], numbers[index + 1])
5 8 8 3 3 10
Диапазон здесь сокращён на единицу, и это не случайность. В списке из четырёх элементов последний индекс равен 3; для него выражение numbers[index + 1] потребовало бы несуществующий индекс 4. Поэтому цикл останавливают на предпоследней позиции.
индексы: 0 1 2 3 значения: 5 8 3 10 пары: └──┘ └──┘ └──┘ ✗ нет следующего
Если внутри используется смещение index + k, диапазон нужно сократить так, чтобы последний индекс со смещением оставался допустимым: range(len(items) - k).
§9Задача на соседей
Приём с парами применяют там, где важна не сама величина, а изменение между соседними измерениями. Найдём максимальный скачок температуры между соседними днями.
temperatures = [12, 15, 21, 19, 26] max_jump = 0 for index in range(len(temperatures) - 1): jump = temperatures[index + 1] - temperatures[index] if jump > max_jump: max_jump = jump print(max_jump)
7
| index | Пара | jump | max_jump после шага |
|---|---|---|---|
0 | 12 → 15 | 3 | 3 |
1 | 15 → 21 | 6 | 6 |
2 | 21 → 19 | -2 | 6 |
3 | 19 → 26 | 7 | 7 |
Обычным перебором значений такую задачу не решить: на каждом шаге нужны сразу два элемента, а for value in temperatures выдаёт только один.
§10Обратный перебор
Индексы позволяют идти по списку с конца. Для этого range() задают с отрицательным шагом.
letters = ["a", "b", "c"] for index in range(len(letters) - 1, -1, -1): print(index, letters[index])
2 c 1 b 0 a
| Аргумент | Значение | Смысл |
|---|---|---|
start | len(letters) - 1 → 2 | последний допустимый индекс |
stop | -1 | граница не включается, поэтому 0 ещё обрабатывается |
step | -1 | движение к меньшим индексам |
Если нужны только значения и не нужны позиции, короче написать for letter in reversed(letters). Индексная форма выигрывает тогда, когда с конца требуется ещё и что-то изменять.
§11Границы списка и IndexError
Допустимые индексы списка идут от 0 до len(items) - 1. Обращение к позиции за этой границей останавливает программу.
numbers = [10, 20, 30] print(numbers[3])
Traceback (most recent call last):
File "main.py", line 2, in <module>
print(numbers[3])
~~~~~~~^^^
IndexError: list index out of range
Последняя строка называет тип ошибки, а стрелки под кодом показывают, какое именно обращение оказалось неверным. В списке из трёх элементов позиции — 0, 1 и 2; индекс 3 лежит уже за концом.
Отсчитывать с конца можно отрицательными индексами: -1 — последний элемент, -2 — предпоследний.
prices = [100, 250, 80] print(prices[-1]) print(prices[len(prices) - 1])
80 80
Обе записи дают последний элемент. Форма prices[-1] короче и читается лучше, а len(prices) - 1 нужна тогда, когда позиция участвует в вычислениях.
Соблазн «дотянуться до последнего элемента» прибавлением единицы приводит к обратному: range(len(items)) уже включает последний индекс, а + 1 добавляет несуществующий.
§12enumerate(): индекс и значение
Если позиция нужна только для вывода или проверки, а не для записи, удобнее enumerate(). Он выдаёт сразу пару: индекс и элемент.
colors = ["red", "green", "blue"] for index, color in enumerate(colors): print(index, color)
0 red 1 green 2 blue
Для нумерованного списка на экране удобен аргумент start: он меняет только выводимые номера, но не позиции в списке.
for number, color in enumerate(colors, start=1): print(number, color)
1 red 2 green 3 blue
| Что нужно | Запись |
|---|---|
| только элементы | for color in colors: |
| индекс для изменения или соседей | for index in range(len(colors)): |
| индекс и значение для чтения | for index, color in enumerate(colors): |
Подробнее enumerate() разбирается в главе 3.14. Важно помнить, что переменная значения в нём ведёт себя так же, как в обычном цикле: присваивание в неё список не изменит — для этого всё равно нужна запись colors[index] = ....
§13Когда нужны индексы
- изменить элементы исходного списка;
- обратиться к предыдущему или следующему элементу;
- сравнить позиции в алгоритме сортировки или поиска;
- обработать только определённый диапазон позиций;
- пройти список в обратном порядке и что-то в нём поменять;
- работать одновременно с двумя списками одинаковой длины.
Если ни одна из этих причин не подходит, обычный перебор элементов читается проще и ошибиться в нём почти невозможно.
§14Частые ошибки
| Что написано | Что произойдёт | Как правильно |
|---|---|---|
for number in numbers: number *= 2 | исходный список не изменится, ошибки при этом не будет | присваивать в numbers[index] |
range(len(numbers) + 1) | последний индекс выйдет за границу, IndexError | range(len(numbers)) |
numbers[index + 1] при полном диапазоне | IndexError на последнем элементе | использовать range(len(numbers) - 1) |
print(index) вместо print(numbers[index]) | выведутся номера позиций, а не значения | обращаться к списку по индексу |
for index in range(numbers) | TypeError: range() ждёт число, а не список | range(len(numbers)) |
range(len(numbers), 0, -1) для обратного перебора | первый же индекс выйдет за границу | range(len(numbers) - 1, -1, -1) |
| индексный цикл без необходимости | код становится длиннее и хуже читается | перебирать элементы напрямую |
§15Проверьте себя
Что создаёт range(len(numbers))?
Допустимые индексы списка от нуля до len(numbers) - 1.
Почему в строке numbers[index] = numbers[index] * 2 одна и та же запись означает разное?
Справа от знака = скобки читают значение из ячейки, слева — указывают ячейку для записи. Сначала вычисляется правая часть, затем результат кладётся в позицию.
Почему for number in numbers: number *= 2 не меняет список?
number — отдельная переменная, в которую скопировано значение. Присваивание перепривязывает имя, а ячейка списка остаётся прежней.
Зачем сокращать диапазон при обращении к index + 1?
Чтобы для последней итерации следующий индекс продолжал существовать.
Как перебрать список от последнего элемента к первому?
for index in range(len(items) - 1, -1, -1). Граница -1 не включается, поэтому нулевая позиция обрабатывается.
Когда лучше обычный for number in numbers?
Когда нужен только сам элемент и позиция не используется.
§16Шпаргалка
range(len(items))items[index]items[index] = значениеitems[index + 1]range(len(items) - 1)range(len(items) - 1, -1, -1)items[-1]enumerate(items)Итог главы
len() даёт размер, range() превращает его в допустимые индексы, а items[index] получает или изменяет элемент. Ключевое отличие индексного перебора в том, что он адресует ячейку списка, а не копию значения, — поэтому только через него список можно менять на месте. Использовать его стоит тогда, когда задача действительно зависит от позиции.