Не выводятся все значения списка
Есть вот такой код. По идее ожидаю того, что принт будет выводить каждые элементы, но он выводит только 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
Ответы (3 шт):
Запомните, что изменять список в процессе итерации по нему - это очень плохая практика, потому что чревато многими 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
обходим в обратном порядке:
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:]
Анализ
Перепишем код с заменой 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 = [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]
На деле это способ повторяет классический (который с del). Не удивлюсь если он окажется самым быстрым - меньше кода, быстрее работает интерпретатор.