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

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

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

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


person postfuturist    schedule 20.08.2008    source источник
comment
Ваш код удаляет несколько Смитов или вы его редактировали?   -  person Geoffrey    schedule 20.07.2010


Ответы (13)


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

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

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

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

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

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

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

person John    schedule 20.08.2008
comment
not name.endswith("Smith") выглядит намного лучше :-) - person Jochen Ritzel; 07.12.2009
comment
конечно, если вам нравится читаемость или что-то в этом роде. - person John; 08.12.2009
comment
Может кто-нибудь объяснить мне [-5:]. Что произойдет, если вы захотите проверить весь список? - person Robert Johnstone; 14.09.2011
comment
@Sevenearths: [-5:] берет последние пять символов имени, так как мы хотим знать, заканчивается ли имя на Smith. Как предложил Йохен, выражение name [: - 5]! = 'Smith' можно было бы более читабельно записать как not name.endswith ('Smith'). - person Edward Loper; 14.11.2011
comment
Не забудьте упомянуть об увеличении производительности за счет использования name.endswith("Smith") вместо [-5:] - person notbad.jpeg; 03.07.2014
comment
@ notbad.jpeg: Микрооптимизация не важна, но есть производительность decrease при использовании name.endswith("Smith") по сравнению с индексированием. Протестируйте его (я сделал)! - person Gerrat; 23.04.2015
comment
@Gerrat Спасибо за исправление. Я думал, что endswith() будет иметь лучшую производительность, поскольку это более питонический способ сравнения, и он может использовать преимущества оптимизации, потому что ему не нужно делать субкопию строки. - person notbad.jpeg; 29.04.2015

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

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

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

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

person Xavier Martinez-Hidalgo    schedule 08.10.2008
comment
Это действительно инновационное и питоническое решение. Я люблю это! - person richo; 07.12.2009
comment
Работает ли это, если в списке есть дубликаты (соответствующие предикату)? - person Jon-Eric; 04.09.2012
comment
@ Джон-Эрик: да, работает. Если есть дубликат, то первый удаляется, список сокращается, и reversed() дает то же name во второй раз. Это алгоритм O (n ** 2), в отличие от принятого ответа, который использует алгоритм O (n). - person jfs; 24.05.2014

Очевидный ответ - тот, который дал Джон и еще пара человек, а именно:

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

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

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

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

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

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

person Edward Loper    schedule 09.01.2011
comment
Действительно интересное открытие производительности. Не могли бы вы рассказать подробнее о своей среде профилирования и методах оценки? - person Drake Guan; 02.03.2012
comment
Готов поспорить, вы могли бы сделать это еще быстрее, используя not name.endswith('Smith') вместо создания среза на каждой итерации. В любом случае, это ценная информация, которую я, вероятно, никогда не нашел бы, если бы не ваш ответ, спасибо. - person notbad.jpeg; 03.07.2014
comment
предложение names[:] было особенно полезно для использования с os.walk для фильтрации имен каталогов для прохождения - person wowest; 18.06.2015

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

list = [x for x in list if x[-5:] != "smith"]
person Corey    schedule 20.08.2008
comment
На самом деле, похоже, не работает для целых чисел. temprevengelist = 0-12354-6876 temprevengelist = temprevengelist.split ('-') list = [x for x in temprevengelist if x [-5:]! = 6876] - person Fahim Akhter; 28.01.2010
comment
@FahimAkhter: Это потому, что вы сравниваете целое число со строкой: в Python 6876 (int) и "6876" (строка) - это два разных значения, и они не равны. Попробуйте заменить x[-5:] != 6876 на x[-5:] != "6876" или int(x[-5:]) != 6876 - person Edward Loper; 20.04.2012

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

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

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

person elifiner    schedule 05.10.2008

filter было бы здорово для этого. Простой пример:

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

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

person mk.    schedule 20.08.2008

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

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

person PabloG    schedule 20.08.2008
comment
Я думаю, что names.remove (name) может быть операцией O (n), которая сделает этот алгоритм O (n ^ 2). - person postfuturist; 04.10.2008
comment
Я бы лично написал свое выражение while как item ‹len (names), на всякий случай, если я испортил логику внутри цикла. (хотя это не похоже на вас) - person Miquella; 08.10.2008
comment
Вероятно, более эффективно использовать del names [элемент] или names.pop (элемент), чем names.remove (имя). Это гораздо менее вероятно, чем O (n), хотя я не знаю, как это работает на самом деле. - person rjmunro; 05.11.2008

Чтобы ответить на ваш вопрос о работе со словарями, обратите внимание, что 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'])
person Jason Baker    schedule 07.10.2008
comment
вам не нужно помещать () вокруг выражений генератора, если только они не являются единственным аргументом и [] заставляет выражение генератора материализовать список, что делает dict([(k,v) for k,v in d.items()]) намного медленнее, чем dict(((k,v) for k,v in d.items())) - person Dan D.; 04.03.2011

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

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)]
person valyala    schedule 02.04.2012
comment
для удаления конечных элементов: del original_list[new_list_size:] - person jfs; 03.03.2013

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

  • Они делают копию вашего списка и возвращают новый, и это будет неэффективно, если исходный список действительно большой.
  • Они могут быть очень громоздкими, когда критерии выбора предметов (в вашем случае, если 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

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

person Ricardo Reyes    schedule 02.10.2008
comment
Это не работает должным образом, если у вас более одной записи Smith, потому что дополнительные экземпляры для удаления были смещены из-за удаления более ранних экземпляров. По той же причине этот алгоритм вызывает исключение, если в конец списка добавляется вторая запись «Смит». - person Miquella; 08.10.2008
comment
@Miquella: вы правы, мой исходный пост не удался для нескольких Смитов, я исправил его, выполняя удаление в обратном порядке. Спасибо. - person Ricardo Reyes; 10.10.2008

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

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

Вот моя filter_inplace реализация, которую можно использовать для фильтрации элементов из списка на месте. Я придумал это самостоятельно, до того, как нашел эту страницу. Это тот же алгоритм, что и опубликованный PabloG, только более общий, чтобы вы могли использовать его для фильтрации списков на месте, он также может удалять из списка на основе comparisonFunc, если установлено обратное 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
person Cory Gross    schedule 15.03.2013

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

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

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

person nlucaroni    schedule 20.08.2008
comment
Для некоторых проблем переключение на другую структуру данных на самом деле не вариант - в частности, если вы не знаете условия фильтрации до тех пор, пока не будет создан набор элементов. Например, если вы выполняете какой-то поиск и хотите сократить пространство поиска, вы, как правило, не знаете заранее подходящее условие отсечения для сокращения. - person Edward Loper; 09.01.2011