Как удалить элемент из списка в Python?
Запутался во всех способах удаления из списка. Есть remove(), pop(), ещё del видел. Чем они отличаются и когда что брать? И самое странное: пробую удалять чётные числа прямо в цикле for, а часть элементов остаётся в списке:
nums = [1, 2, 2, 3, 4, 4]
for n in nums:
if n % 2 == 0:
nums.remove(n)
print(nums) # ожидал [1, 3], получил что-то другое
Почему так и как сделать правильно?
3 ответа
Разберём по полочкам, потому что у тебя тут сразу две темы: способы удаления и баг с итерацией.
Три способа удалить:
lst = ['a', 'b', 'c', 'b']
lst.remove('b') # удаляет ПЕРВОЕ вхождение значения 'b'
print(lst) # ['a', 'c', 'b']
x = lst.pop(0) # удаляет по ИНДЕКСУ и ВОЗВРАЩАЕТ элемент
print(x, lst) # a ['c', 'b']
del lst[0] # удаляет по индексу, ничего не возвращает
print(lst) # ['b']
Коротко: remove — когда знаешь значение, pop — когда знаешь индекс и хочешь забрать элемент (например, как стек: stack.pop() без аргумента снимает последний), del — когда индекс знаешь, а возвращать не нужно (ещё умеет срезы: del lst[1:3]).
Теперь твой баг. Ты меняешь список, по которому прямо сейчас идёшь. Под капотом for хранит внутренний индекс. Удалил элемент — всё, что правее, сдвинулось влево на одну позицию, а индекс шагнул вперёд. В итоге один элемент перепрыгивается. Поэтому второй 2 и второй 4 остаются.
Правильно — не мутировать при итерации, а собрать новый список:
nums = [1, 2, 2, 3, 4, 4]
nums = [n for n in nums if n % 2 != 0]
print(nums) # [1, 3]
Если нужно изменить именно тот же объект (на него ссылаются в других местах), используй срез-присваивание — оно перезаписывает содержимое:
nums[:] = [n for n in nums if n % 2 != 0]
List comprehension тут и быстрее, и читается лучше любого цикла с remove.
Добавлю частую боль: remove и pop кидают исключения, к этому надо быть готовым.
lst = [1, 2, 3]
lst.remove(99) # ValueError: list.remove(x): x not in list
lst.pop(10) # IndexError: pop index out of range
Поэтому если не уверен, что значение есть:
if 99 in lst:
lst.remove(99)
И ещё: remove убирает только ПЕРВОЕ вхождение. Чтобы выкинуть все одинаковые — фильтрация через comprehension, как выше, а не цикл с remove в надежде, что он повторится.
Если очень хочется удалять в цикле «на месте» — итерируйся по копии, а меняй оригинал:
nums = [1, 2, 2, 3, 4, 4]
for n in nums[:]: # nums[:] — копия
if n % 2 == 0:
nums.remove(n)
print(nums) # [1, 3]
Работает, но это O(n^2) из-за remove (он линейно ищет значение). На больших списках comprehension заметно быстрее. Так что копию используй только когда логика удаления действительно сложная.