Удаление из списка, итерации по нему

Следующий код:

a = list(range(10))
remove = False
for b in a:
    if remove:
        a.remove(b)
    remove = not remove
print(a)

Вывод [0, 2, 3, 5, 6, 8, 9] вместо [0, 2, 4, 6, 8] при использовании Python 3.2.

  • Почему он выводит эти конкретные значения?
  • Почему не указана ошибка, указывающая, что основной итератор изменяется?
  • Изменились ли механики из более ранних версий Python в отношении этого поведения?

Обратите внимание, что я не ищу работу, но чтобы понять ее.

Ответ 1

Я обсуждал это некоторое время, потому что подобные вопросы задавались много раз здесь. Но это достаточно просто, чтобы получить преимущество от сомнений. (Тем не менее, я не буду возражать, если другие голосуют, чтобы закрыть.) Вот визуальное объяснение того, что происходит.

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]       <-  b = 0; remove? no
 ^
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]       <-  b = 1; remove? yes
    ^
[0, 2, 3, 4, 5, 6, 7, 8, 9]          <-  b = 3; remove? no
       ^
[0, 2, 3, 4, 5, 6, 7, 8, 9]          <-  b = 4; remove? yes
          ^
[0, 2, 3, 5, 6, 7, 8, 9]             <-  b = 6; remove? no
             ^
[0, 2, 3, 5, 6, 7, 8, 9]             <-  b = 7; remove? yes
                ^
[0, 2, 3, 5, 6, 8, 9]                <-  b = 9; remove? no
                   ^

Поскольку никто другой не имеет, я попытаюсь ответить на ваши другие вопросы:

Почему не указана ошибка, указывающая, что базовый итератор изменяется?

Чтобы выбросить ошибку, не запрещая многие совершенно корректные конструкции циклов, Python должен был бы много узнать о том, что происходит, и, вероятно, придется получить эту информацию во время выполнения. Вся эта информация потребует времени для обработки. Это сделало бы Python намного медленнее, только в том месте, где скорость действительно рассчитывается - цикл.

Изменились ли механики из более ранних версий Python в отношении этого поведения?

Короче говоря, нет. Или, по крайней мере, я очень сомневаюсь в этом, и, конечно, он вел себя так, так как я изучил Python (2.4). Честно говоря, я бы ожидал, что любая простая реализация изменчивой последовательности будет вести себя именно таким образом. Любой, кто знает лучше, пожалуйста, поправьте меня. (На самом деле быстрый поиск документов подтверждает, что текст, который Mikola указан в учебнике, начиная с версия 1.4!)

Ответ 2

Как объяснил Микола, фактический результат, который вы наблюдаете, вызван тем фактом, что удаление записи из списка смещает весь список на одно место, заставляя вас пропускать элементы.

Но более интересный вопрос, на мой взгляд, заключается в том, почему python не выбирает создавать сообщение об ошибке, когда это происходит. Он вызывает такое сообщение об ошибке, если вы попытаетесь изменить словарь. Я думаю, для этого есть две причины.

  • Dict сложны внутри, а списки - нет. Списки в основном представляют собой массивы. Дик должен обнаруживать, когда он изменен во время повторения, чтобы избежать сбоев при изменении внутренней структуры dict. Список может уйти, не выполняя эту проверку, потому что он просто убеждает, что его текущий индекс все еще находится в зоне действия.

  • Исторически (я не уверен сейчас) списки python были перезаписаны с помощью оператора []. Python будет оценивать список [0], список [1], список [2], пока не получит IndexError. В этом случае python не отслеживал размер списка до его начала, поэтому у него не было способа обнаружить, что размер списка был изменен.

Ответ 3

Конечно, небезопасно изменять массив, когда вы выполняете итерацию по нему. Спектр говорит, что это плохая идея, а поведение undefined:

http://docs.python.org/tutorial/controlflow.html#for-statements

Итак, следующий вопрос: что именно происходит здесь под капотом? Если бы я должен был догадаться, я бы сказал, что он делает что-то вроде этого:

for(int i=0; i<len(array); ++i)
{
   do_loop_body(i);
}

Если вы предполагаете, что это действительно то, что происходит, то это полностью объясняет наблюдаемое поведение. Когда вы удаляете элемент на или до текущего указателя, вы перемещаете весь список на 1 влево. В первый раз вы удаляете 1 - как обычно - но теперь список сдвигается назад. Следующая итерация вместо удара 2, вы нажмете 3. Затем вы удалите 4, и список сдвинется назад. Следующая итерация 7 и т.д.

Ответ 4

На вашей первой итерации вы не удаляетесь и все денди.

Вторая итерация находится в позиции [1] последовательности, и вы удаляете "1". Итератор затем перенесет вас в позицию [2] в последовательности, которая теперь "3", поэтому "2" пропускается (поскольку "2" теперь находится в позиции [1] из-за удаления). Конечно, "3" не удаляется, поэтому вы переходите к позиции [3] в последовательности, которая теперь "4". Это удаляется, вы попадаете на позицию [5], которая теперь "6" и т.д.

Тот факт, что вы удаляете вещи, означает, что позиция пропускается каждый раз, когда вы выполняете удаление.