Неожиданное поведение при зацикливании списка

У меня есть список из четырех чисел:

mylist=[3,5,67,4]

Я хочу удалить все нечетные числа. Итак, я написал следующее:

for item in mylist:
  if item%2==1:
    mylist.remove(item)

Когда я печатаю mylist, я получаю следующее:

[5,4]

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

for item in mylist:
  if item%2==1:
    mylist.remove(item)
  print mylist

который дает:

[4]

Что тут происходит? Что мне не хватает?


person Fezter    schedule 04.06.2013    source источник
comment
Это распространенная ошибка, которая возникает, когда вы изменяете последовательность во время ее повторения.   -  person mgilson    schedule 04.06.2013
comment
Конечно. Я не знаю, почему я этого не видел. Я понял теперь. Кроме того, я согласен с тем, что это дублирующий вопрос, и я пометил его как таковой.   -  person Fezter    schedule 04.06.2013


Ответы (3)


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

Лучшее решение – создать новый список, лучше всего с пониманием списка:

mylist = [item for item in mylist if item % 2 == 0]

Если вам нужно изменить список, вы можете впоследствии присвоить значения обратно (mylist[:] = ...), но маловероятно, что вам нужно изменять его на месте.

Это также имеет преимущество быть читабельным и кратким.

person Gareth Latty    schedule 04.06.2013
comment
более надежным решением будет mylist[:] = mylist[::2]. Иначе в другой ситуации он может случайно добавить локальную привязку. - person Elazar; 04.06.2013
comment
@Elazar: Это не решает исходную проблему, заключающуюся в удалении всех нечетных чисел. Это просто возвращает список, содержащий все остальные элементы из исходного списка. - person Justin S Barrett; 04.06.2013
comment
@Elazar Это не решает проблему, и в большинстве случаев нет необходимости фактически изменять существующий список, как я указал в ответе. - person Gareth Latty; 04.06.2013
comment
@Elazar Я пояснил, что это вариант, но, как я уже отмечал, маловероятно, что это нужно делать на месте, а возвращение обратно — менее эффективная операция, поэтому это следует делать только в случае необходимости. быть сделано. - person Gareth Latty; 04.06.2013
comment
Я в порядке с вашим исправленным ответом. Но я думаю, что эффективность вызывает беспокойство только в том случае, если это явно так. - person Elazar; 04.06.2013
comment
@Elazar Преждевременная оптимизация не имеет смысла, но здесь мы говорим о случае, когда это проще сделать более оптимизированным способом. Изменение списка, скорее всего, будет недостатком, чем необходимо. В большинстве случаев работа над новым списком является правильным путем. На самом деле, код, который полагается на изменение списка на месте, вероятно, ненадежен и, вероятно, нуждается в изменении. Если кому-то нужно это сделать, они будут знать, что им это нужно. - person Gareth Latty; 04.06.2013
comment
OK. Справедливо. Но вы делаете новый список, у него должно быть новое имя. - person Elazar; 04.06.2013
comment
@Elazar Если вам больше не нужен старый список, нет причин не использовать старое имя повторно и не позволять старому быть сборщиком мусора. - person Gareth Latty; 04.06.2013
comment
Есть. у вас есть глобальный список mylist, вы выполняете задание, и вдруг вместо этого вы ссылаетесь на локальный список. - person Elazar; 04.06.2013
comment
Это очень специфический случай, да, в этом случае было бы лучше использовать другое имя. Тем не менее, глобальные переменные в любом случае обычно плохая идея. Я действительно не вижу в этом вашей точки зрения - да, есть некоторые случаи, когда вы хотели бы назначить обратно или использовать другое имя, но это общий ответ, я не могу надеяться дать его для каждого возможного сценария. - person Gareth Latty; 04.06.2013
comment
вы можете: использовать подходящее имя. новые имена для новых списков, старые имена для старых списков. изменение на месте, если это необходимо, учитывая, что это, вероятно, не так. Этот оператор присваивания может быть не тем, что вы имели в виду (и не только с глобальными переменными), поэтому его следует избегать, если это возможно. - person Elazar; 04.06.2013
comment
Я не согласен - нет ничего плохого в повторном использовании имени. Это зависит от ситуации. Да, естественно, вы должны изменить ответ в соответствии со своими потребностями, но это данность с любым ответом на SO. - person Gareth Latty; 04.06.2013
comment
Спасибо за этот ответ. +1 за ссылку на ваше отличное видео. - person Fezter; 04.06.2013

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

for item in mylist[:]:
    if item%2==1:
        mylist.remove(item)
person Justin S Barrett    schedule 04.06.2013

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

Мой код на Python устарел, поэтому вместо этого я буду использовать C-подобный псевдокод...

for ( i = lastindex; i >= 0; --i )
  if ( some condition involving list[i] )
    remove item at index i

Это работает при удалении из списка, потому что элементы, перемещенные при удалении, — это те, которые вы уже просмотрели; ваш индекс i все еще действителен, как и все элементы, которые вы еще не оценили!

person aldo    schedule 04.06.2013
comment
Цикл по индексу не Pythonic — он негибкий (не работает с итераторами, только с последовательностями), его сложнее читать и он медленный. - person Gareth Latty; 04.06.2013