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


50

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

Это innefficient, довольно некрасиво и, возможно, глючит (как это обрабатывать несколько «Джон Смит '). У кого-нибудь есть более элегантное решение или, по крайней мере, более эффективное?

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

  0

Ваш код удаляет несколько Smiths или вы его отредактировали? 20 июл. 102010-07-20 12:24:30

52

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

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

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

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

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

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

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

  0

Кроме того, выражения генератора. 07 окт. 082008-10-07 18:50:11

+12

'not name.endswith (« Смит »)' выглядит намного приятнее :-) 07 дек. 092009-12-07 04:13:47

+5

уверен, если вам нравится читаемость или что-то в этом роде. 07 дек. 092009-12-07 22:31:09

  0

Может кто-нибудь объяснить '[-5:]' мне. Что произойдет, если вы хотите проверить весь список? 14 сен. 112011-09-14 15:45:49

+2

@Sevenearths: «[-5:]» берет последние пять символов имени, так как мы хотим знать, заканчивается ли имя «Смит». Как предположил Йохен, выражение «имя [: - 5]! =« Смит »может быть написано более читаемо как« not name.endswith («Смит») ». 14 ноя. 112011-11-14 14:57:43

  0

Не забудьте указать увеличение производительности с помощью 'name.endswith (« Smith »)' вместо '[-5:]' 03 июл. 142014-07-03 01:24:11

  0

@ notbad.jpeg: микро-оптимизация не важна, но есть производительность 'убывает', используя' name.endswith («Смит») 'против индексации. Оцените это (я сделал)! 23 апр. 152015-04-23 15:21:16

  0

@Gerrat Спасибо за исправление. Я думал, что 'endswith()' будет иметь лучшую производительность, так как это более питонический способ сравнения, и он может использовать преимущества оптимизации, потому что ему не нужно делать подкопию строки. 29 апр. 152015-04-29 16:05:29


-2

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

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

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

+1

Для некоторых проблем переключение на другую структуру данных на самом деле не является вариантом - в частности, если вы не знаете условия фильтра до тех пор, пока не будет создан набор элементов. Например, если вы делаете какой-то поиск и хотите сократить свое пространство поиска, вы, как правило, не будете знать подходящее условие отсечки для вашей обрезки заранее. 09 янв. 112011-01-09 14:44:26


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

3

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

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

Edit: Кори список понимание является удивительным тоже.


10

Использование a list comprehension

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

На самом деле не работает целых чисел. temprevengelist = "0-12354-6876" temprevengelist = temprevengelist.split ('-') list = [x for x in temprevengelist, если x [-5:]!= 6876] 28 янв. 102010-01-28 07:48:53

  0

@FahimAkhter: Это потому, что вы сравниваете целое число со строкой: в Python '6876' (int) и' "6876" '(строка) являются двумя разными значениями и не равны. Попробуйте заменить 'x [-5:]! = 6876' либо с помощью' x [-5:]! = "6876" или 'int (x [-5:])! = 6876' 20 апр. 122012-04-20 19:33:11


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 

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

  0

Я думаю, что names.remove (name) может быть операцией O (n), которая сделает этот алгоритм O (n^2). 04 окт. 082008-10-04 03:28:27

+1

Я бы лично написал свое выражение while как item <len (names), на всякий случай, я испортил логику внутри цикла. (хотя это не похоже на то, что вы сделали) 08 окт. 082008-10-08 00:31:51

  0

Возможно, более эффективно использовать имена доменов [item] или names.pop (item), чем names.remove (name). Скорее всего, это O (n), хотя я не знаю фактических внутренних элементов, как это работает. 05 ноя. 082008-11-05 13:11:18


1

Фильтр и списочные одобрены для примера, но у них есть несколько проблем:

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

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

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 

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

  0

Это не работает должным образом, если у вас более одной записи «Смит», потому что дополнительные экземпляры для удаления были смещены из-за удаления более ранних экземпляров. И по той же причине этот алгоритм вызывает исключение, если вторая запись «Смит» добавляется в конец списка. 08 окт. 082008-10-08 00:37:46

  0

@Miquella: вы правы, мое исходное сообщение не удалось для нескольких Смитов, я исправил его удаление в обратном порядке. Благодарю. 10 окт. 082008-10-10 18:12:03


4

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

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

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


2

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

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

В то же время, вы можете сделать квази-Dict понимание так:

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

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

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

вам не нужно помещать '()' вокруг выражений генератора, если они не являются единственным аргументом, а '[]' делает выражение генератора материализуемым списком, что и делает 'dict ([(k, v) для k , v в d.items()]) 'настолько медленнее, чем' dict (((k, v) для k, v в d.items())) 04 мар. 112011-03-04 10:40:38


37

Вы также можете иТЭР поел назад по списку:

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

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

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

  0

Решил мою проблему, спасибо :) 24 дек. 082008-12-24 13:03:09

  0

Это действительно инновационного и Pythonic решения. Я люблю это! 07 дек. 092009-12-07 04:13:11

  0

oo pretty clever 12 авг. 102010-08-12 17:06:45

  0

Это работает, если в списке есть дубликаты (совпадающие с предикатом)? 04 сен. 122012-09-04 16:58:06

  0

@ Jon-Eric: да, это работает. Если есть дубликат, первый из них удаляется, список сжимается, а 'reverse()' дает то же самое имя во второй раз. Это алгоритм O (n ** 2) в отличие от [принятого ответа] (http://stackoverflow.com/a/18435/4279), который использует алгоритм O (n). 23 май. 142014-05-23 21:18:15


1

В случае комплекта.

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

28

Ответ очевиден тот, что Джон и несколько других людей, дали, а именно:

>>> 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% быстрее, чем подход фильтра.

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

+2

Действительно интересное открытие производительности. Не могли бы вы рассказать больше о своей среде профилирования и методах оценки? 02 мар. 122012-03-02 01:39:34

  0

Бьюсь об заклад, вы можете сделать это еще быстрее, используя 'not name.endswith ('Smith')' вместо того, чтобы создавать срез на каждой итерации. В любом случае, это ценная информация, которую я, вероятно, никогда не нашел, если бы не ваш ответ, спасибо. 03 июл. 142014-07-03 01:27:44

+1

предложение 'names [:]' было особенно полезно для использования с 'os.walk' для фильтрации dirnames для перемещения 18 июн. 152015-06-18 02:49:23


2

Если список должен быть отфильтрован на месте, а размер списка довольно большой , то алгоритмы, упомянутые в предыдущих ответах, которые основаны на 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 

Edit: На самом деле, решение по 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)] 
  0

для удаления конечных элементов:' del original_list [new_list_size:] ' 03 мар. 132013-03-03 06:23:45


1

Вот моя 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