Удаление элемента из списка - во время итерации - что случилось с этой идиомой?

В качестве эксперимента я сделал это:

letters=['a','b','c','d','e','f','g','h','i','j','k','l']
for i in letters:
    letters.remove(i)
print letters

Последняя печать показывает, что не все элементы были удалены? (каждый другой был).

IDLE 2.6.2      
>>> ================================ RESTART ================================
>>> 
['b', 'd', 'f', 'h', 'j', 'l']
>>> 

Какое объяснение этому? Как это можно было бы переписать для удаления каждого элемента?

Ответы

Ответ 1

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


В чем причина этого?

Потому что язык Python разработан так, чтобы по-разному обрабатывать этот вариант использования. Документация проясняет:

Не безопасно модифицировать последовательность, повторяемую в цикле (это может происходить только для изменяемых типов последовательностей, таких как списки). Если вам нужно изменить список, который вы перебираете (например, для дублирования выбранных элементов), вы должны перебрать копию.

Акцент мой.См. Связанную страницу для получения дополнительной информации - документация защищена авторским правом и все права защищены.

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

Это все равно, что удивляться, почему i += i++ + ++i делает, черт возьми, то, что делает эта строка в вашей архитектуре для вашей конкретной сборки компилятора для вашего языка - включая, помимо прочего, уничтожение вашего компьютера и создание демонов вылетит из твоего носа :)


Как это может быть переписано, чтобы удалить каждый элемент?

  • del letters[:] (если вам нужно изменить все ссылки на этот объект)
  • letters[:] = [] (если вам нужно изменить все ссылки на этот объект)
  • letters = [] (если вы просто хотите работать с новым объектом)

Может быть, вы просто хотите удалить некоторые элементы в зависимости от условия? В этом случае вам следует перебрать копию списка. Самый простой способ сделать копию - создать фрагмент, содержащий весь список с синтаксисом [:], например так:

#remove unsafe commands
commands = ["ls", "cd", "rm -rf /"]
for cmd in commands[:]:
  if "rm " in cmd:
    commands.remove(cmd)

Если ваша проверка не особенно сложна, вы можете (и, вероятно, должны) фильтровать вместо этого:

commands = [cmd for cmd in commands if not is_malicious(cmd)]

Ответ 2

Вы не можете перебирать список и мутировать его одновременно, вместо этого перебирайте фрагмент:

letters=['a','b','c','d','e','f','g','h','i','j','k','l']
for i in letters[:]: # note the [:] creates a slice
     letters.remove(i)
print letters

Тем не менее, для простой операции, такой как это, вы должны просто использовать:

letters = []

Ответ 3

Вы не можете изменить список, который вы выполняете, иначе вы получите этот странный тип результата. Чтобы сделать это, вы должны перебрать копию списка:

for i in letters[:]:
  letters.remove(i)

Ответ 4

что вы хотите сделать:

letters[:] = []

или

del letters[:]

Это сохранит исходный объект, на который указывал letters. Другие параметры, такие как letters = [], создадут для него новый объект и точку letters: старый объект обычно будет собираться через мусор через некоторое время.

Причина, по которой не все значения были удалены, заключается в том, что вы меняете список, итерации по нему.

ETA: если вы хотите отфильтровать значения из списка, вы можете использовать следующие методы:

>>> letters=['a','b','c','d','e','f','g','h','i','j','k','l']
>>> [l for l in letters if ord(l) % 2]
['a', 'c', 'e', 'g', 'i', 'k']

Ответ 5

Он удаляет первое вхождение, а затем проверяет следующее число в последовательности. Поскольку последовательность изменилась, она принимает следующее нечетное число и так далее...

  • возьмите "a"
  • удалить "a" → первый элемент теперь "b"
  • возьмите следующий элемент, c) -...

Ответ 6

Вероятно, python использует указатели, и удаление начинается с фронта. Переменные "буквы" во второй строке частично имеют другое значение, чем переменные "буквы" в третьей строке. Когда я равно 1, тогда a удаляется, когда я равно 2, затем b перемещается в положение 1, а c удаляется. Вы можете попытаться использовать "while".

Ответ 7

    #!/usr/bin/env python
    import random
    a=range(10)

    while len(a):
        print a
        for i in a[:]:
            if random.random() > 0.5:
                print "removing: %d" % i
                a.remove(i)
            else:
                print "keeping: %d"  % i           

    print "done!"
    a=range(10)

    while len(a):
        print a
        for i in a:
            if random.random() > 0.5:
                print "removing: %d" % i
                a.remove(i)
            else:
                print "keeping: %d"  % i           

    print "done!"

Я думаю, что это объясняет проблему немного лучше, верхний блок кода работает, тогда как нижний не делает.

Элементы, которые "хранятся" в нижнем списке, никогда не распечатываются, потому что вы модифицируете список, который вы повторяете, что является рецептом для катастрофы.

Ответ 8

Хорошо, я немного опоздал на вечеринку здесь, но я думал об этом и, посмотрев код реализации Python (CPython), получил объяснение, которое мне нравится. Если кто-нибудь знает, почему это глупо или неправильно, я был бы рад услышать, почему.

Проблема перемещается по списку с помощью итератора, позволяя изменить этот список.

Весь итератор обязан сделать, это указать вам, какой элемент в списке (в этом случае) появляется после текущего элемента (т.е. со следующей() функцией).

Я верю, что итераторы в настоящее время реализованы, они только отслеживают индекс последнего элемента, который они повторили. Глядя в iterobject.c, можно увидеть, что похоже на определение итератора:

typedef struct {
    PyObject_HEAD
    Py_ssize_t it_index;
    PyObject *it_seq; /* Set to NULL when iterator is exhausted */
} seqiterobject;

где it_seq указывает на повторную последовательность и it_index дает индекс последнего элемента, предоставленного итератором.

Когда итератор только что отправил элемент n th и один из них удалит этот элемент из последовательности, изменится соответствие между последующими элементами списка и их индексами. Первый элемент (n + 1) st становится элементом n th в отношении итератора. Другими словами, итератор теперь думает, что то, что было "следующим" элементом в последовательности, фактически является "текущим" элементом.

Таким образом, когда его попросят дать следующий элемент, он даст первый элемент (n + 2) nd (т.е. новый (n + 1) st элемент).

В результате для рассматриваемого кода метод iterator next() будет давать только элементы n + 0, n + 2, n + 4,... из исходного списка. Элементы n + 1, n + 3, n + 5,... никогда не будут отображаться в инструкции remove.

Несмотря на то, что предполагаемая деятельность рассматриваемого кода ясна (по крайней мере для человека), вероятно, потребуется гораздо больше интроспекции для итератора для отслеживания изменений в последовательности, в которой она выполняется, и затем действовать в " человеческая мода.

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