Модуль 3 · Списки и циклы

Справочник по Python · модуль 3 · списки и циклы

Глава 3.6

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 по очереди получает каждую позицию

Выражение читается изнутри наружу.

  1. len(numbers) вычисляется первым и даёт количество элементов — 4.
  2. range(4) превращает количество в набор позиций 0, 1, 2, 3.
  3. Цикл for перебирает этот набор и кладёт очередное число в переменную index.
Почему именно len(), а не 4

Число 4 пришлось бы исправлять вручную при каждом изменении списка. len(numbers) вычисляется при запуске и подходит любому списку, включая пустой: тогда range(0) пуст, и тело цикла не выполнится ни разу.

§3Индекс и элемент

index хранит позицию, а numbers[index] получает элемент на этой позиции. Это два разных значения, и путать их — самая частая ошибка в теме.

main.pyпозиция рядом со значением
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ВыражениеРезультат
0numbers[0]10
1numbers[1]20
2numbers[2]30
3numbers[3]40

Здесь список только читается и потому не меняется. Дальше начинается запись, и таблицу придётся дополнить состоянием списка.

§5Изменение элемента

Главная строка всей главы выглядит так:

Строкачтение и запись в одной позиции
numbers[index] = numbers[index] * 2

Слева и справа стоит одна и та же запись numbers[index], но работает она по-разному. Python вычисляет присваивание справа налево.

  1. Сначала полностью вычисляется правая часть. Запись numbers[index] справа означает «взять значение из ячейки»: при index = 0 получится 1.
  2. Полученное значение умножается на 2 — получается 2. Список в этот момент ещё не изменён.
  3. Затем срабатывает левая часть. Запись numbers[index] слева означает «положить значение в ячейку» — результат записывается в позицию 0 списка.
Схемаодна запись — две роли
numbers[index] = numbers[index] * 2
      │                  │
      │                  └ ② прочитать значение из позиции
      └ ③ записать результат в ту же позицию

Иначе говоря, квадратные скобки слева от знака = адресуют ячейку, а справа — достают её содержимое. Именно поэтому старое значение успевает поучаствовать в вычислении нового.

main.pyудвоение на каждой позиции
numbers = [1, 2, 3, 4]

for index in range(len(numbers)):
    numbers[index] = numbers[index] * 2

print(numbers)
Выводисходный список изменён
[2, 4, 6, 8]

§6Трассировка изменения

Чтобы увидеть механику целиком, полезно расписать все четыре повтора и состояние списка после каждого из них.

indexПрочитано справаВычисленоСписок после шага
011 * 2 = 2[2, 2, 3, 4]
122 * 2 = 4[2, 4, 3, 4]
233 * 2 = 6[2, 4, 6, 4]
344 * 2 = 8[2, 4, 6, 8]

Список меняется постепенно, по одной ячейке за повтор. Новый список не создаётся: всё это время numbers указывает на одну и ту же коллекцию, у которой обновляется содержимое.

Сокращённая запись

Строку можно записать короче: numbers[index] *= 2. Результат тот же, порядок действий тот же. Начинающим полезнее сначала освоить полную форму, чтобы видеть чтение и запись явно.

§7Почему обычный for не меняет список

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

main.pyпопытка без индексов
numbers = [1, 2, 3, 4]

for number in numbers:
    number = number * 2

print(numbers)
Выводсписок остался прежним
[1, 2, 3, 4]

Причина в том, что number — это отдельная переменная цикла, а не сама ячейка списка. На каждом повторе в неё копируется значение элемента.

numbers[0]→1
number→1

Строка number = number * 2 присваивает новое значение имени number. Связь между этим именем и позицией 0 при этом теряется: переменная начинает указывать на число 2, а ячейка списка по-прежнему хранит 1.

numbers[0]→1
number→2

На следующем повторе number просто перезаписывается значением следующего элемента, и вычисленная двойка теряется бесследно. Чтобы изменение попало в список, адресовать нужно ячейку — то есть писать numbers[index] = ....

Ошибки не будет — будет неверный результат

Это опасный случай: программа отрабатывает молча, вывод выглядит правдоподобно, и заметить проблему можно только по значениям. Если после цикла список «почему-то не изменился» — первым делом проверьте, присваиваете вы в переменную или в позицию списка.

§8Соседние элементы

Вторая причина использовать индексы: имея позицию, можно обратиться и к соседним — index - 1 и index + 1.

main.pyсоседние пары
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Задача на соседей

Приём с парами применяют там, где важна не сама величина, а изменение между соседними измерениями. Найдём максимальный скачок температуры между соседними днями.

main.pyмаксимальный прирост
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Параjumpmax_jump после шага
012 → 1533
115 → 2166
221 → 19-26
319 → 2677

Обычным перебором значений такую задачу не решить: на каждом шаге нужны сразу два элемента, а for value in temperatures выдаёт только один.

§10Обратный перебор

Индексы позволяют идти по списку с конца. Для этого range() задают с отрицательным шагом.

main.pyот последней позиции к нулевой
letters = ["a", "b", "c"]

for index in range(len(letters) - 1, -1, -1):
    print(index, letters[index])
Выводиндексы убывают
2 c
1 b
0 a
АргументЗначениеСмысл
startlen(letters) - 1 → 2последний допустимый индекс
stop-1граница не включается, поэтому 0 ещё обрабатывается
step-1движение к меньшим индексам

Если нужны только значения и не нужны позиции, короче написать for letter in reversed(letters). Индексная форма выигрывает тогда, когда с конца требуется ещё и что-то изменять.

§11Границы списка и IndexError

Допустимые индексы списка идут от 0 до len(items) - 1. Обращение к позиции за этой границей останавливает программу.

main.pyвыход за границу
numbers = [10, 20, 30]
print(numbers[3])
Ошибкаиндекс 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 — предпоследний.

main.pyотсчёт с конца
prices = [100, 250, 80]

print(prices[-1])
print(prices[len(prices) - 1])
Выводдве записи одного элемента
80
80

Обе записи дают последний элемент. Форма prices[-1] короче и читается лучше, а len(prices) - 1 нужна тогда, когда позиция участвует в вычислениях.

range(len(items) + 1) — источник IndexError

Соблазн «дотянуться до последнего элемента» прибавлением единицы приводит к обратному: range(len(items)) уже включает последний индекс, а + 1 добавляет несуществующий.

§12enumerate(): индекс и значение

Если позиция нужна только для вывода или проверки, а не для записи, удобнее enumerate(). Он выдаёт сразу пару: индекс и элемент.

main.pyдве переменные в заголовке цикла
colors = ["red", "green", "blue"]

for index, color in enumerate(colors):
    print(index, color)
Выводнумерация с нуля
0 red
1 green
2 blue

Для нумерованного списка на экране удобен аргумент start: он меняет только выводимые номера, но не позиции в списке.

main.pyнумерация с единицы
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)последний индекс выйдет за границу, IndexErrorrange(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] получает или изменяет элемент. Ключевое отличие индексного перебора в том, что он адресует ячейку списка, а не копию значения, — поэтому только через него список можно менять на месте. Использовать его стоит тогда, когда задача действительно зависит от позиции.

Глава 3.6 · Перебор списка по индексам · Справочник по Python Автор: Макаров Максим Николаевич