Операция вычитания списка Python

227

Я хочу сделать что-то похожее на это:

>>> x = [1,2,3,4,5,6,7,8,9,0]  
>>> x  
[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]  
>>> y = [1,3,5,7,9]  
>>> y  
[1, 3, 5, 7, 9]  
>>> y - x   # (should return [2,4,6,8,0])

Но это не поддерживается списками Python. Каков наилучший способ сделать это?

мечтатель
источник
@ezdazuzena это не вычитание. В этом разница между двумя списками. Ваша публикация не является дублированием этого вопроса.
Celik
1
Что должны вернуть [2, 2] - [2]? []? [2]?
Маккей
@McKay [2,2] - [2] должен вернуть [2]. [2,2] - [1,2,2,3] должна возвращать []
Робиньо
Этот вопрос касается вычитания списка, но принятый ответ ближе к установленному вычитанию.
Робиньо
2
Что должны вернуть [2, 1, 2, 3, 2, 4, 2] - [2, 3, 2] и почему? Должен ли он найти 232 в середине и вернуть 2142? или он должен каждый раз находить первое и возвращать 1242? Или что-то другое? Я говорю о том, что это не очевидные ответы и они зависят от необходимости.
Маккей

Ответы:

330

Используйте понимание списка:

[item for item in x if item not in y]

Если вы хотите использовать -синтаксис инфикса, вы можете просто сделать:

class MyList(list):
    def __init__(self, *args):
        super(MyList, self).__init__(args)

    def __sub__(self, other):
        return self.__class__(*[item for item in self if item not in other])

затем вы можете использовать его как:

x = MyList(1, 2, 3, 4)
y = MyList(2, 5, 2)
z = x - y   

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

aaronasterling
источник
10
@admica, не используйте listдля имен переменных, поскольку это скрывает listконструктор. Если вы используете «список», пожалуйста, поставьте перед ним подчеркивание. Кроме того, отбросив *, вы взломали мой код ...
aaronasterling
19
Если вы это сделаете, [1,1,2,2] - [1,2]вы получите пустой список. [1,1,2,2] - [2]дает [1,1]Так что это на самом деле не вычитание списка, это больше похоже на «Список из списка X без элементов из набора Y » .
Альфред Зиен
@AlfredZien то, что он сказал
RetroCode
Метод понимания списка намного медленнее (в моем примере), чем метод задания разностей.
Редфилу
1
@BarnabasSzabolcs: Это ничего не спасет, потому что перед каждой проверкой он будет преобразован yв (что аналогично стоимости оригинальной работы). Вам нужно было бы либо выполнить за пределами listcomp, затем протестировать , либо в качестве вопиющего хакерства , которое использует вложенные списки списков для кэширования как однострочного. Немного менее уродливое однострочное решение, которое работает адекватно, будет состоять в использовании, потому что аргумент to создается только один раз. setyset = set(y)if item not in yset[item for yset in [set(y)] for item in x if item not in yset]ysetlist(itertools.filterfalse(set(y).__contains__, x))filterfalse
ShadowRanger
259

Использовать установленную разницу

>>> z = list(set(x) - set(y))
>>> z
[0, 8, 2, 4, 6]

Или вы можете просто установить x и y, чтобы вам не приходилось делать какие-либо преобразования.

quantumSoup
источник
50
это потеряет любой порядок. Это может иметь или не иметь значения в зависимости от контекста.
Ааронастерлинг
63
Это также приведет к потере любых возможных дубликатов, которые могут нуждаться в поддержке.
Опал
Я получаюTypeError: unhashable type: 'dict'
Havnar
Это
гораздо
2
Если порядок и дубликаты элементов в списке не важны для контекста, это отличный ответ, и он очень удобочитаемый.
Ватт Ямсури
37

Это операция «установить вычитание». Используйте для этого заданную структуру данных.

В Python 2.7:

x = {1,2,3,4,5,6,7,8,9,0}
y = {1,3,5,7,9}
print x - y

Вывод:

>>> print x - y
set([0, 8, 2, 4, 6])
Санта
источник
1
list (set ([1,2,3,4,5]) - set ([1,2,3])) = [4, 5], так что сначала перечисляются списки, которые нужно установить, а затем вычесть (или однонаправленную разницу) ) и вернуться к списку.
gseattle
2
Не хорошо, если вы хотите сохранить оригинальный порядок элементов набора x.
Захран
34

Если дубликаты и заказы являются проблемой:

[i for i in a if not i in b or b.remove(i)]

a = [1,2,3,3,3,3,4]
b = [1,3]
result: [2, 3, 3, 3, 4]
нгуен
источник
2
Это работает, хотя это время O(m * n)выполнения (и я съеживаюсь всякий раз, когда listcomp включает побочные эффекты); вы можете улучшить его, используяcollections.Counter для получения O(m + n)времени выполнения.
ShadowRanger
Мне трудно это понять, может кто-нибудь объяснить?
Аннушка
20

Для многих случаев использования вы хотите получить ответ:

ys = set(y)
[item for item in x if item not in ys]

Это гибрид между ответом aaronasterling в и ответ quantumSoup в .

Версия aaronasterling выполняет len(y)сравнение элементов для каждого элемента x, поэтому требуется квадратичное время. В версии QuantumSoup используются наборы, поэтому для каждого элемента выполняется поиск по одному набору с постоянным временем, xно, поскольку он преобразует оба x и yв наборы, он теряет порядок ваших элементов.

Преобразуя только yв набор и повторяя xпо порядку, вы получаете лучшее из обоих миров - линейного времени и сохранения порядка. *


Однако в версии QuantumSoup все еще есть проблема: она требует, чтобы ваши элементы были хэшируемыми. Это в значительной степени встроено в природу наборов. ** Если вы пытаетесь, например, вычесть список диктов из другого списка, но список для вычитания велик, что вы делаете?

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

ys = {tuple(item.items()) for item in y}
[item for item in x if tuple(item.items()) not in ys]

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


Если ваши элементы не являются и не могут быть сделаны хэшируемыми, но они сопоставимы, вы можете, по крайней мере, получить логарифмическое время ( O(N*log M)что намного лучше, чем O(N*M)время решения списка, но не так хорошо, как O(N+M)время заданного раствора) путем сортировки и с помощью bisect:

ys = sorted(y)
def bisect_contains(seq, item):
    index = bisect.bisect(seq, item)
    return index < len(seq) and seq[index] == item
[item for item in x if bisect_contains(ys, item)]

Если ваши элементы не являются ни хэшируемыми, ни сопоставимыми, то вы застряли с квадратичным решением.


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

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

abarnert
источник
7

Поиск значений в наборах происходит быстрее, чем поиск в списках:

[item for item in x if item not in set(y)]

Я считаю, что это будет немного лучше, чем:

[item for item in x if item not in y]

Оба сохраняют порядок списков.

rudolfbyker
источник
Будет ли он кэшироваться set(y)и не преобразовываться yв новый набор в каждом цикле? В противном случае, вы бы ответ нужно abarnert в: ys = set(y); [i for i in x if i not in ys].
Джектоз
2
Некоторые грубые тесты показывают, что это if i not in set(y)занимает на 25% больше времени if i not in y( чем yсписок). Предварительное преобразование набора занимает на 55% меньше времени. Протестировано с довольно короткими xи y, но различия должны стать более выраженными с длиной, во всяком случае.
Джектоз
1
@Jacktose: Да, это решение делает больше работы, потому что оно должно повторять и хэшировать каждый элемент yдля каждого элемента x; если сравнение на равенство не является действительно дорогим по сравнению с вычислением хеша, это всегда будет проигнорировано item not in y.
ShadowRanger
@ShadowRanger, который имеет смысл. Если бы преобразование набора было надежно более быстрым способом выполнить эту проверку, вы бы подумали, что компилятор всегда будет выполнять проверку таким образом.
Джектоз
5

Если списки допускают дублирование элементов, вы можете использовать Counter из коллекций:

from collections import Counter
result = list((Counter(x)-Counter(y)).elements())

Если вам нужно сохранить порядок элементов из x:

result = [ v for c in [Counter(y)] for v in x if not c[v] or c.subtract([v]) ]
Алена Т.
источник
Это хорошо, хотя и теряет порядок; исправить это немного сложнее .
ShadowRanger
@ ShadowRanger, это действительно так. но совсем чуть-чуть
Ален Т.
Не обращайте на меня внимания, я просто собираюсь содрогнуться в списках рассылки с кешированием и побочными эффектами (хотя я полагаю, что комбинация этих двух снимает видимые снаружи побочные эффекты?). :-)
ShadowRanger
Кроме того, этот код не будет работать так, как написано; Counter.subtractне удаляет элементы с нулевым значением ( -и не -=делает subtract), поэтому вы никогда не прекратите удалять элементы. Вы хотите заменить not v in cна not c[v](который возвращает ноль для несуществующих элементов, так что вы можете безопасно проверить возврат на «нулевое значение» через not).
ShadowRanger
@ShadowRanger, хороший улов! Исправлено сейчас.
Ален Т.
3

Я думаю, что самый простой способ добиться этого - использовать set ().

>>> x = [1,2,3,4,5,6,7,8,9,0]  
>>> y = [1,3,5,7,9]  
>>> list(set(x)- set(y))
[0, 2, 4, 6, 8]
Loochie
источник
3

Другие решения имеют одну из нескольких проблем:

  1. Они не сохраняют порядок или
  2. Они не удаляют точное количество элементов, например, for x = [1, 2, 2, 2]и y = [2, 2]преобразуют yв a set, и либо удаляют все совпадающие элементы (оставляя [1]только), либо удаляют один из каждого уникального элемента (оставляя [1, 2, 2]), когда правильное поведение будет удалять 2дважды, оставляя [1, 2]или
  3. Они O(m * n)работают, где оптимальное решение может O(m + n)работать

Ален был на правильном пути,Counter чтобы решить # 2 и # 3, но это решение потеряет порядок. Решение, которое сохраняет порядок (удаление первых nкопий каждого значения для nповторений в listзначениях для удаления):

from collections import Counter

x = [1,2,3,4,3,2,1]  
y = [1,2,2]  
remaining = Counter(y)

out = []
for val in x:
    if remaining[val]:
        remaining[val] -= 1
    else:
        out.append(val)
# out is now [3, 4, 3, 1], having removed the first 1 and both 2s.

Попробуйте онлайн!

Чтобы удалить последние копии каждого элемента, просто измените forцикл на for val in reversed(x):и добавьте out.reverse()сразу после выхода из forцикла.

Построение Counterвыражается O(n)в единицах yдлины, итерации x- O(n)в единицах xдлины, а Counterтестирование членства и мутации O(1)пока list.appendамортизируются O(1)(данные appendмогут быть O(n), но для многих appendс, общие средние значения big-O, O(1)так как все меньше и меньше из них требуют перераспределения), поэтому общая работа сделана O(m + n).

Вы также можете проверить, чтобы определить, были ли какие-либо элементы y, которые не были удалены xпри тестировании:

remaining = +remaining  # Removes all keys with zero counts from Counter
if remaining:
    # remaining contained elements with non-zero counts
ShadowRanger
источник
Примечание: Это действительно требует значений быть hashable, но любое решение , которое не требует hashable объектов либо не общее назначение (например , можно рассчитывать intс на массив фиксированной длиной) или должен сделать больше , чем O(m + n)работы (например , следующим лучшим масштабно -O будет состоять в том, чтобы отсортировать listпары уникальных значений / счетчиков, превратив O(1) dictпоиски в O(log n)бинарные поиски; вам понадобятся уникальные значения с их счетами, а не просто отсортированные неуникальные значения, потому что в противном случае вы бы заплатили O(n)расходы на удаление элементы из отсортированного list).
ShadowRanger
2

Попробуй это.

def subtract_lists(a, b):
    """ Subtracts two lists. Throws ValueError if b contains items not in a """
    # Terminate if b is empty, otherwise remove b[0] from a and recurse
    return a if len(b) == 0 else [a[:i] + subtract_lists(a[i+1:], b[1:]) 
                                  for i in [a.index(b[0])]][0]

>>> x = [1,2,3,4,5,6,7,8,9,0]
>>> y = [1,3,5,7,9]
>>> subtract_lists(x,y)
[2, 4, 6, 8, 0]
>>> x = [1,2,3,4,5,6,7,8,9,0,9]
>>> subtract_lists(x,y)
[2, 4, 6, 8, 0, 9]     #9 is only deleted once
>>>
user3435376
источник
1

Ответ предоставляется @aaronasterling выглядит хорошо, однако, он не совместим с интерфейсом по умолчанию списка: x = MyList(1, 2, 3, 4)против x = MyList([1, 2, 3, 4]). Таким образом, приведенный ниже код может использоваться как более дружественный к списку Python:

class MyList(list):
    def __init__(self, *args):
        super(MyList, self).__init__(*args)

    def __sub__(self, other):
        return self.__class__([item for item in self if item not in other])

Пример:

x = MyList([1, 2, 3, 4])
y = MyList([2, 5, 2])
z = x - y
Хамид Зафар
источник
0

Я думаю это быстрее

In [1]: a = [1,2,3,4,5]

In [2]: b = [2,3,4,5]

In [3]: c = set(a) ^ set(b)

In [4]: c
Out[4]: {1}
Eds_k
источник
Это не вычитание. Фактически это симметричная разница между двумя списками.
Парф Чаухан
Более того, это работает только для хэшируемых объектов внутри списков
zhukovgreen
-1

Этот пример вычитает два списка:

# List of pairs of points
list = []
list.append([(602, 336), (624, 365)])
list.append([(635, 336), (654, 365)])
list.append([(642, 342), (648, 358)])
list.append([(644, 344), (646, 356)])
list.append([(653, 337), (671, 365)])
list.append([(728, 13), (739, 32)])
list.append([(756, 59), (767, 79)])

itens_to_remove = []
itens_to_remove.append([(642, 342), (648, 358)])
itens_to_remove.append([(644, 344), (646, 356)])

print("Initial List Size: ", len(list))

for a in itens_to_remove:
    for b in list:
        if a == b :
            list.remove(b)

print("Final List Size: ", len(list))
Жоао Николау
источник
8
Избегайте этого, это O (N ^ 2)
Александр - Восстановите Монику