Не выводятся все значения списка

Рейтинг: 4Ответов: 3Опубликовано: 19.03.2023

Есть вот такой код. По идее ожидаю того, что принт будет выводить каждые элементы, но он выводит только 1, 3, 5

a = [1, 2, 3, 4, 5, 6]
    
for i in a:
    print(i)
    if i % 2 != 0:
        a.remove(i)


# 1
# 3
# 5

Ответы

▲ 4Принят

Запомните, что изменять список в процессе итерации по нему - это очень плохая практика, потому что чревато многими side-эффектами.


Что произошло в данном случае?

На первой итерации: i = 1, i % 2 != 0 -> True, поэтому удаляется элемент 1 из списка.

В результате чего, список изменится и будет уже другим : [2, 3, 4, 5, 6], далее, для дальнейших рассуждений, нужно понимать, как происходит итерация по списку.

Если коротко: на каждой итерации выбирается элемент следующий по индексу (вызывается магический метод __next__).

На первой итерации был возвращен элемент с индексом 0, соответственно на второй итерации - с индексом 1, и т.д

Но в вашем примере есть один нюанс, вы изменили исходный список в процессе итерации по нему, поэтому переход будет выполнен на элемент с индексом 1 уже измененного списка [2, 3, 4, 5, 6] - то есть 3, в следствие чего был пропущен элемент со значением 2, так как итератор ничего не знает о том, что исходный список был изменен, и просто сдвигает указатель на следующий индекс.


Наглядная демонстрация

Итерация 1

[1, 2, 3, 4, 5, 6] -> [2, 3, 4, 5, 6]
 ^                     ^
 i -> print(1)         i

Итерация 2
 
[2, 3, 4, 5, 6]
 x  ^                     
    i -> print(3)                    

Как же решить задачу без side-эффектов?

Для удаления (фильтрации) элементов по условию можно использовать, например, списковые включения:

a = [1, 2, 3, 4, 5, 6]

a = [x for x in a if x % 2 != 0]

print(a)

Или встроенную функцию filter():

a = [1, 2, 3, 4, 5, 6]

a = filter(lambda x: x % 2 != 0, a)

print(list(a))

Вывод:

[1, 3, 5]

Но это же будет уже новый список?

Да, ссылка изменится, но есть возможность сохранить обновленный список по старой ссылке:

a = [1, 2, 3, 4, 5, 6]
print(id(a)) # 2089295957248

a[:] = (x for x in a if x % 2 != 0)
print(id(a)) # 2089295957248
▲ 3

обходим в обратном порядке:

for i in reversed(range(len(a))):
    print(a[i])
    if a[i] % 2:
        a.pop(i)

Более выгодно не перестраивать список каждый раз, а "сжимать" его и изменить длину один раз в конце

cnt = 0
for i in range(len(a)):
    if a[i] % 2:
        cnt += 1
    else:
        a[i-cnt] = a[i]
del a[-cnt:]
▲ 2

Анализ

Перепишем код с заменой for на while. j - перебирает индексы в списке. Печать доработана чтобы выводить j и список:

a = [1, 2, 3, 4, 5, 6]

j = 0
while j < len(a):
    i = a[j]
    # print(i)
    print(f'j = {j}, a = {a}, a[j] = {a[j]}')
    if i % 2 != 0:
        a.remove(i)
    j += 1

Когда условие истинно, список a укорачивается и счётчик j увеличивается. Пропускаются элементы:

j = 0, a = [1, 2, 3, 4, 5, 6], a[j] = 1
j = 1, a = [2, 3, 4, 5, 6], a[j] = 3
j = 2, a = [2, 4, 5, 6], a[j] = 5

Копирование исходного списка

Проблема от того что меняется список по которому ведётся цикл. Правим оригинальный цикл. Видите a[:] в заголовке цикла?. Теперь итерация ведётся по копии списка и все неприятности исчезают:

a = [1, 2, 3, 4, 5, 6]
    
for i in a[:]:
    print(i)
    if i % 2 != 0:
        a.remove(i)

Точное управление счётчиком в while

Вариант с while тоже можно доработать. Он обходится без копирования:

a = [1, 2, 3, 4, 5, 6]

j = 0
while j < len(a):
    i = a[j]
    # print(i)
    print(f'j = {j}, a = {a}, a[j] = {a[j]}')
    if i % 2 != 0:
        a.remove(i)
    else:
        j += 1
j = 0, a = [1, 2, 3, 4, 5, 6], a[j] = 1
j = 0, a = [2, 3, 4, 5, 6], a[j] = 2
j = 1, a = [2, 3, 4, 5, 6], a[j] = 3
j = 1, a = [2, 4, 5, 6], a[j] = 4
j = 2, a = [2, 4, 5, 6], a[j] = 5
j = 2, a = [2, 4, 6], a[j] = 6

Хотя всё это работает, я бы не стал так делать. Работает медленно на длинных списках (квадратичная сложность в терминах О-большого). А последний код ещё и сложно устроен.

Классика

Индекс j будет отставать от итераций в цикле for всякий раз когда очередное значение в списке нечётное. В этом случае список тоже меняется во время итерации, но не меняется его длина и "хвост". Этого достаточно, чтобы сюрпризов не было.

Так как во время итерации мы не меняем длину списка в конце его надо обрезать (del):

a = [1, 2, 3, 4, 5, 6]

j = 0
for i in a:
    print(i)
    if i % 2 == 0:
        a[j] = i
        j += 1
del a[j:]

Классика на Питоне

В Питоне есть ещё один способ отфильтровать список "на месте". К сожалению, тут нужна двойная память: выражение a[:] = (...) создаёт список из генератора в правой части:

a = [1, 2, 3, 4, 5, 6]
print(id(a), a)
a[:] = (i for i in a if i % 2 == 0)
print(id(a), a)
139721591755968 [1, 2, 3, 4, 5, 6]
139721591755968 [2, 4, 6]

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