Элегантный способ удалить элементы из последовательности в Python?

Asked
Viewd40436

55

Когда я пишу код на Python, мне часто нужно удалить элементы из списка или другого типа последовательности на основе некоторых критериев. Я не нашел элегантного и эффективного решения, так как удаление элементов из списка, который вы сейчас просматриваете, - это плохо. Например, вы не можете этого сделать:

 for name in names:
    if name[-5:] == 'Smith':
        names.remove(name)
 

Обычно я делаю что-то вроде этого:

 toremove = []
for name in names:
    if name[-5:] == 'Smith':
        toremove.append(name)
for name in toremove:
    names.remove(name)
del toremove
 

Это неэффективно, довольно некрасиво и, возможно, содержит ошибки (как он обрабатывает несколько записей «Джон Смит»?). Есть ли у кого-нибудь более элегантное решение или хотя бы более эффективное?

Как насчет того, что работает со словарями?

14 ответов

56

Два простых способа выполнить только фильтрацию:

  1. Использование filter:

    names = filter(lambda name: name[-5:] != "Smith", names)

  2. Использование списков:

    names = [name for name in names if name[-5:] != "Smith"]

Обратите внимание, что в обоих случаях сохраняются значения, для которых функция предиката оценивает True, поэтому вам нужно изменить логику (т. е. вы говорите «оставить людей, у которых нет фамилии Смит», а не «удалить людей, которые иметь фамилию Смит »).

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

10

Использование понимания списка

 list = [x for x in list if x[-5:] != "smith"]
 
3
Фильтр

отлично подойдет для этого. Простой пример:

 names = ['mike', 'dave', 'jim']
filter(lambda x: x != 'mike', names)
['dave', 'jim']
 

Изменить: Кори тоже прекрасно понимает список.

2

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

 names = ['Jones', 'Vai', 'Smith', 'Perez']

item = 0
while item <> len(names):
    name = names [item]
    if name=='Smith':
        names.remove(name)
    else:
        item += 1

print names
 

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

-2

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

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

edit: понимаешь, что он просил «эффективности» ... все эти предложенные методы просто перебирают список, что совпадает с тем, что он предложил.

2
 names = filter(lambda x: x[-5:] != "Smith", names);
 
2

Чтобы ответить на ваш вопрос о работе со словарями, обратите внимание, что Python 3.0 будет включать понимание слов :

 >>> {i : chr(65+i) for i in range(4)}
 

А пока вы можете выполнить квази-диктовку следующим образом:

 >>> dict([(i, chr(65+i)) for i in range(4)])
 

Или, как более прямой ответ:

 dict([(key, name) for key, name in some_dictionary.iteritems if name[-5:] != 'Smith'])
 
4

Бывают случаи, когда фильтрация (с использованием фильтра или понимания списка) не работает. Это происходит, когда какой-либо другой объект содержит ссылку на список, который вы изменяете, и вам нужно изменить список на месте.

 for name in names[:]:
    if name[-5:] == 'Smith':
        names.remove(name)
 

Единственное отличие от исходного кода - это использование names[:] вместо names в цикле for. Таким образом, код перебирает (неглубокую) копию списка, и удаления работают, как ожидалось. Поскольку копирование списка неглубокое, оно довольно быстрое.

1

Понимание фильтров и списков подходит для вашего примера, но у них есть несколько проблем:

  • Они делают копию вашего списка и возвращают новый, и это будет неэффективно, если исходный список действительно большой.
  • Они могут быть очень громоздкими, если критерии выбора предметов (в вашем случае, если name [-5:] == 'Smith') более сложны или имеют несколько условий.

Ваше исходное решение на самом деле более эффективно для очень больших списков, даже если мы можем согласиться с его уродливостью. Но если вы беспокоитесь, что у вас может быть несколько «Джон Смит», это можно исправить, удалив на основе позиции, а не значения:

 names = ['Jones', 'Vai', 'Smith', 'Perez', 'Smith']

toremove = []
for pos, name in enumerate(names):
    if name[-5:] == 'Smith':
        toremove.append(pos)
for pos in sorted(toremove, reverse=True):
    del(names[pos])

print names
 

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

37

Вы также можете перебирать список назад:

 for name in reversed(names):
    if name[-5:] == 'Smith':
        names.remove(name)
 

Это имеет то преимущество, что он не создает новый список (например, filter или понимание списка) и использует итератор вместо копии списка (например, [:]).

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