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






Что ж, это явно проблема с используемой вами структурой данных. Например, используйте хеш-таблицу. Некоторые реализации поддерживают несколько записей для каждой клавиши, поэтому можно либо удалить самый новый элемент, либо удалить их все.
Но это - и то, что вы собираетесь найти, - это элегантность за счет другой структуры данных, а не алгоритма. Может быть, у вас получится лучше, если он отсортирован, или что-то в этом роде, но итерация по списку - ваш единственный метод здесь.
редактировать: можно понять, что он просил «эффективности» ... все эти предложенные методы просто перебирают список, что совпадает с тем, что он предложил.
Для некоторых проблем переключение на другую структуру данных на самом деле не вариант - в частности, если вы не знаете условие фильтра до тех пор, пока не будет создан набор элементов. Например, если вы выполняете какой-то поиск и хотите сократить пространство поиска, вы, как правило, не знаете заранее подходящее условие отсечения для сокращения.
names = filter(lambda x: x[-5:] != "Smith", names);
filter было бы здорово для этого. Простой пример:
names = ['mike', 'dave', 'jim']
filter(lambda x: x != 'mike', names)
['dave', 'jim']
Редактировать: Понимание списка Кори тоже потрясающе.
Использование понимание списка
list = [x for x in list if x[-5:] != "smith"]
На самом деле, похоже, не работает для целых чисел. temprevengelist = "0-12354-6876" temprevengelist = temprevengelist.split ('-') list = [x for x in temprevengelist if x [-5:]! = 6876]
@FahimAkhter: Это потому, что вы сравниваете целое число со строкой: в Python 6876 (int) и "6876" (строка) - это два разных значения, и они не равны. Попробуйте заменить x[-5:] != 6876 на x[-5:] != "6876" или int(x[-5:]) != 6876.
Два простых способа выполнить только фильтрацию:
Используя filter:
names = filter(lambda name: name[-5:] != "Smith", names)
Использование списков:
names = [name for name in names if name[-5:] != "Smith"]
Обратите внимание, что в обоих случаях сохраняются значения, для которых функция предиката оценивает True, поэтому вам нужно изменить логику (т.е. вы говорите «оставить людей, у которых нет фамилии Смит», а не «удалить людей, у которых есть последняя фамилия»). имя Смит ").
Редактировать Забавно ... два человека по отдельности опубликовали оба ответа, которые я предложил, когда я писал свой.
not name.endswith("Smith") выглядит намного лучше :-)
конечно, если вам нравится читаемость или что-то в этом роде.
Может кто-нибудь объяснить мне [-5:]. Что произойдет, если вы захотите проверить весь список?
@Sevenearths: «[-5:]» берет последние пять символов имени, так как мы хотим знать, заканчивается ли имя на «Smith». Как предположил Йохен, выражение «name [: - 5]! = 'Smith'» можно было бы более читабельно записать как «not name.endswith ('Smith')».
Не забудьте упомянуть об увеличении производительности за счет использования name.endswith("Smith") вместо [-5:].
@ notbad.jpeg: Микрооптимизация не важна, но есть производительность decrease при использовании name.endswith("Smith") по сравнению с индексированием. Протестируйте его (я сделал)!
@Gerrat Спасибо за исправление. Я думал, что endswith() будет иметь лучшую производительность, поскольку это более питонический способ сравнения, и он может использовать преимущества оптимизации, потому что ему не нужно делать субкопию строки.
Оба решения, фильтр и понимание, требуют создания нового списка. Я не знаю достаточно о внутреннем устройстве 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
В любом случае, для кратких списков я придерживаюсь любого из двух предложенных ранее решений.
Я думаю, что names.remove (name) может быть операцией O (n), которая сделает этот алгоритм O (n ^ 2).
Я бы лично написал свое выражение while как item
Вероятно, более эффективно использовать del names [элемент] или names.pop (элемент), чем names.remove (имя). Это гораздо менее вероятно, чем O (n), хотя я не знаю, как это работает на самом деле.
Понимание фильтров и списков подходит для вашего примера, но у них есть пара проблем:
Ваше исходное решение на самом деле более эффективно для очень больших списков, даже если мы можем согласиться с его уродливостью. Но если вы беспокоитесь, что у вас может быть несколько '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
Мы не можем выбрать решение, не учитывая размер списка, но для больших списков я бы предпочел ваше двухпроходное решение вместо фильтра или понимания списков.
Это не работает должным образом, если у вас есть более одной записи Smith, потому что дополнительные экземпляры для удаления были смещены из-за удаления более ранних экземпляров. По той же причине этот алгоритм вызывает исключение, если в конец списка добавляется вторая запись «Смит».
@Miquella: вы правы, мой исходный пост не удался для нескольких Смитов, я исправил его, выполняя удаление в обратном порядке. Спасибо.
Бывают случаи, когда фильтрация (с использованием фильтра или понимания списка) не работает. Это происходит, когда какой-либо другой объект содержит ссылку на список, который вы изменяете, и вам нужно изменить список на месте.
for name in names[:]:
if name[-5:] == 'Smith':
names.remove(name)
Единственное отличие от исходного кода - это использование names[:] вместо names в цикле for. Таким образом, код выполняет итерацию по (неглубокой) копии списка, и удаления работают, как ожидалось. Поскольку копирование списка неглубокое, оно довольно быстрое.
Чтобы ответить на ваш вопрос о работе со словарями, обратите внимание, что 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'])
вам не нужно помещать () вокруг выражений генератора, если они не являются единственным аргументом, а [] заставляет выражение генератора материализовать список, что делает dict([(k,v) for k,v in d.items()]) намного медленнее, чем dict(((k,v) for k,v in d.items()))
Вы также можете перебирать список в обратном направлении:
for name in reversed(names):
if name[-5:] == 'Smith':
names.remove(name)
Это имеет то преимущество, что он не создает новый список (например, filter или понимание списка) и использует итератор вместо копии списка (например, [:]).
Обратите внимание, что, хотя удаление элементов во время итерации в обратном направлении безопасно, их вставка несколько сложнее.
Это действительно инновационное и питоническое решение. Я люблю это!
Работает ли это, если в списке есть дубликаты (соответствующие предикату)?
@ Джон-Эрик: да, работает. Если есть дубликат, то первый удаляется, список сокращается, и reversed() возвращает тот же name во второй раз. Это алгоритм O (n ** 2), в отличие от принятый ответ, который использует алгоритм O (n).
В случае набора.
toRemove = set([])
for item in mySet:
if item is unwelcome:
toRemove.add(item)
mySets = mySet - toRemove
Очевидный ответ - тот, который дал Джон и еще пара человек, а именно:
>>> 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 * и использовал его для удаления точек поиска из луча поиска.)
Действительно интересное открытие производительности. Не могли бы вы рассказать подробнее о своей среде профилирования и методах оценки?
Бьюсь об заклад, вы могли бы сделать это еще быстрее, используя not name.endswith('Smith') вместо создания среза на каждой итерации. В любом случае, это ценная информация, которую я, вероятно, никогда бы не нашел, если бы не ваш ответ, спасибо.
предложение names[:] было особенно полезно для использования с os.walk для фильтрации имен каталогов для прохождения
Если список должен быть отфильтрован на месте, а размер списка довольно велик, то алгоритмы, упомянутые в предыдущих ответах, основанные на 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)]
для удаления конечных элементов: del original_list[new_list_size:]
Вот моя реализация 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
Ваш код удаляет несколько Смитов или вы его редактировали?