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

StackOverflow https://stackoverflow.com/questions/18418

Вопрос

На этот вопрос уже есть ответ здесь:

Когда я пишу код на 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

Это неэффективно, довольно уродливо и, возможно, глючно (как он обрабатывает несколько записей "John Smith"?).Есть ли у кого-нибудь более элегантное решение или, по крайней мере, более эффективное?

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

Это было полезно?

Решение

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

  1. Используя filter:

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

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

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

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

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

Другие советы

Вы также можете выполнить итерацию в обратном направлении по списку:

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

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

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

Очевидный ответ - это тот, который дали Джон и пара других людей, а именно:

>>> names = [name for name in names if name[-5:] != "Smith"]       # <-- slower

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

>>> names[:] = (name for name in names if name[-5:] != "Smith")    # <-- faster

Присвоение "именам [:]" в основном означает "заменить содержимое списка имен следующим значением".Это отличается от простого присвоения имен тем, что не создает новый объект списка.Правая часть присваивания представляет собой выражение-генератор (обратите внимание на использование круглых, а не квадратных скобок).Это заставит Python выполнять итерацию по списку.

Некоторое быстрое профилирование предполагает, что это примерно на 30% быстрее, чем подход к пониманию списка, и примерно на 40% быстрее, чем подход к фильтрации.

Предостережение:хотя это решение быстрее, чем очевидное, оно более неясное и опирается на более продвинутые методы Python.Если вы все-таки используете его, я рекомендую сопроводить его комментарием.Вероятно, его стоит использовать только в тех случаях, когда вы действительно заботитесь о производительности этой конкретной операции (которая выполняется довольно быстро, несмотря ни на что).(В случае, когда я использовал это, я выполнял поиск по * лучу и использовал это для удаления точек поиска из поискового луча.)

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

list = [x for x in list if x[-5:] != "smith"]

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

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

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

фильтр был бы отличным решением для этого.Простой пример:

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

Редактировать: Понимание Кори списка тоже потрясающее.

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

Оба решения, Фильтр и понимание требуется создать новый список.Я недостаточно разбираюсь во внутренних компонентах 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

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

Чтобы ответить на ваш вопрос о работе со словарями, вы должны отметить, что 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'])

Если список должен быть отфильтрован на месте, а размер списка довольно большой, то алгоритмы, упомянутые в предыдущих ответах, которые основаны на list.remove(), могут оказаться непригодными, поскольку их вычислительная сложность равна O (n ^ 2).В этом случае вы можете использовать следующую питоновскую функцию no-so:

def filter_inplace(func, original_list):
  """ Filters the original_list in-place.

  Removes elements from the original_list for which func() returns False.

  Algrithm's computational complexity is O(N), where N is the size
  of the original_list.
  """

  # Compact the list in-place.
  new_list_size = 0
  for item in original_list:
    if func(item):
      original_list[new_list_size] = item
      new_list_size += 1

  # Remove trailing items from the list.
  tail_size = len(original_list) - new_list_size
  while tail_size:
    original_list.pop()
    tail_size -= 1


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

# Remove even numbers from a in-place.
filter_inplace(lambda x: x & 1, a)

# Prints [1, 3, 5, 7]
print a

Редактировать:На самом деле, решение в https://stackoverflow.com/a/4639748/274937 превосходит мое решение.Он более питонический и работает быстрее.Итак, вот новая реализация filter_inplace():

def filter_inplace(func, original_list):
  """ Filters the original_list inplace.

  Removes elements from the original_list for which function returns False.

  Algrithm's computational complexity is O(N), where N is the size
  of the original_list.
  """
  original_list[:] = [item for item in original_list if func(item)]

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

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

Ваше оригинальное решение на самом деле более эффективно для очень больших списков, даже если мы можем согласиться, что оно более уродливое.Но если вы беспокоитесь, что у вас может быть несколько "John 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

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

В случае набора.

toRemove = set([])  
for item in mySet:  
    if item is unwelcome:  
        toRemove.add(item)  
mySets = mySet - toRemove 

Вот мой filter_inplace реализация, которую можно использовать для фильтрации элементов из списка на месте, я придумал это самостоятельно, прежде чем найти эту страницу.Это тот же алгоритм, что и опубликованный в PabloG, только более общий, чтобы вы могли использовать его для фильтрации списков на месте, он также способен удалять из списка на основе comparisonFunc если задано значение reversed (обратное) True;что-то вроде обратного фильтра, если хотите.

def filter_inplace(conditionFunc, list, reversed=False):
    index = 0
    while index < len(list):
        item = list[index]

        shouldRemove = not conditionFunc(item)
        if reversed: shouldRemove = not shouldRemove

        if shouldRemove:
            list.remove(item)
        else:
            index += 1

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

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

Редактировать: человек действительно понимает, что он просил об "эффективности"...все эти предложенные методы просто повторяют список, который совпадает с тем, что он предложил.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top