Удаление из списка во время итерации по нему - PullRequest
12 голосов
/ 28 июня 2011

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

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.

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

Обратите внимание, что я не пытаюсь обойти поведение, но понимаю его.

Ответы [ 4 ]

16 голосов
/ 28 июня 2011

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

[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 должен был бы знать lot о том, что происходит, и он, вероятно, должен был бы получить эту информацию во время выполнения. Вся эта информация займет время для обработки. Это сделало бы Python намного медленнее, просто там, где скорость действительно имеет значение - петля.

Механики изменились по сравнению с более ранними версиями Python по отношению к этому поведению?

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

4 голосов
/ 28 июня 2011

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

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

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

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

3 голосов
/ 28 июня 2011

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

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 и т. Д.

0 голосов
/ 28 июня 2011

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

На второй итерации вы находитесь в позиции [1] последовательности и удаляете «1».Затем итератор переводит вас в позицию [2] в последовательности, которая теперь равна «3», поэтому «2» пропускается (так как «2» теперь находится в позиции [1] из-за удаления).Конечно, «3» не удаляется, поэтому вы переходите к позиции [3] в последовательности, которая теперь равна «4».Это удаляется, переводя вас в положение [5], которое теперь равно 6, и т. Д.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...