Во время обхода списка из него удаляют элементы. Почему следующий элемент после удалённого может быть пропущен?
Итератор списка не создаёт снимок его содержимого: он хранит текущую позицию и обращается к изменяемому списку по индексу. После удаления элементы сдвигаются влево, но позиция итератора увеличивается, поэтому на следующем шаге он может перескочить через элемент.
Поддержка обхода изменяемых коллекций без обязательного копирования позволяет экономить память и избегать лишних затрат на создание снимка. Такой подход удобен, когда коллекция меняется независимо от итератора, но ответственность за безопасную модификацию во время обхода остаётся на разработчике.
Рассмотрим список, из которого удаляют элементы прямо в теле цикла. После удаления длина списка уменьшается, а элементы справа от удалённого получают меньшие индексы.
Если итератор уже перешёл к следующей числовой позиции, сдвинутый элемент на этой позиции будет пропущен. В результате часть элементов не обработается, а итоговое состояние списка может отличаться от ожидаемого.
Итератор списка обычно перемещается по индексам: сначала читает элемент с индексом 0, затем с индексом 1 и так далее. Он не фиксирует исходный набор объектов и не корректирует свою позицию после удаления элемента.
Например, после обработки первого элемента удаляется элемент с индексом 1. Бывший элемент с индексом 2 перемещается на индекс 1, но итератор переходит к индексу 2 — этот объект остаётся необработанным.
Число 3 осталось в списке, но не было проверено телом цикла: после удаления 2 оно сдвинулось на уже пройденную позицию.
Безопасные варианты зависят от задачи:
Модификация словаря или множества во время итерации обычно приводит к RuntimeError, потому что изменение структуры нарушает гарантии их итераторов. Для списка ошибка часто не возникает, но это не означает, что результат корректен.
Сервис получает список идентификаторов отключённых объектов и должен удалить из него все значения, не прошедшие проверку. Разработчик удаляет элементы из исходного списка во время обычного обхода, из-за чего часть соседних идентификаторов не проверяется.
Обход копии решает проблему и сохраняет понятную логику, но удваивает память на время операции. Удаление по индексам с конца экономит копию, однако требует аккуратного управления индексами и хуже читается.
В большинстве случаев выбранным решением будет создание нового списка через списковое включение: оно явно выражает операцию фильтрации, не меняет коллекцию во время обхода и не пропускает элементы. Исходный список можно заменить новым после завершения построения; результат становится предсказуемым и проще тестируется.
Нет. Цикл получает итератор списка, который обходит исходный объект. Поэтому изменения списка видны итератору, но их влияние определяется изменением индексов и длины, а не снимком первоначального содержимого.
Нет. Пропуск зависит от вида изменения и позиции элемента. Удаление перед ещё не обработанными элементами обычно сдвигает их и может привести к пропуску, добавление может сделать новые элементы доступными для обхода, а изменение значения по существующему индексу не меняет длину и не вызывает такого сдвига.
Однако полагаться на конкретный результат сложной последовательности изменений не следует: код становится зависимым от деталей порядка операций. Безопаснее отделять построение результата от изменения исходной коллекции.
Да, присваивание значения по существующему индексу не сдвигает остальные элементы и обычно не нарушает продвижение итератора. Но нужно учитывать семантику задачи: уже обработанный объект может быть заменён, а ещё не обработанный — изменён до момента чтения.
Это отличается от добавления и удаления, которые меняют длину или расположение элементов. Поэтому замена значений и изменение структуры коллекции требуют разных оценок безопасности.