web-dev-qa-db-fra.com

Soustraction de deux listes en Python

En Python, comment peut-on soustraire deux listes non uniques et non ordonnées? Disons que nous avons a = [0,1,2,1,0] et b = [0, 1, 1] J'aimerais faire quelque chose comme c = a - b et que c soit [2, 0] ou [0, 2] l'ordre n'a pas d'importance pour moi. Cela devrait renvoyer une exception si a ne contient pas tous les éléments de b.

Notez que c'est différent des ensembles! Je ne suis pas intéressé à trouver la différence entre les ensembles d'éléments de a et b, je m'intéresse à la différence entre les collections d'éléments réelles de a et b.

Je peux le faire avec une boucle for, en recherchant le premier élément de b dans a puis en supprimant l'élément de b et de a, etc. Mais cela ne me plaît pas, ce serait très inefficace (ordre de O(n^2) time ) alors que cela ne devrait pas poser de problème de le faire dans O(n log n) temps.

41
wich

Python 2.7 et 3.2 ajouteront les collections.Counter class qui est un dictionnaire mappant des éléments sur le nombre d'occurrences de l'élément. Cela peut être utilisé comme un multiset.

Selon la documentation, vous devriez pouvoir faire quelque chose comme ceci (non testé, car aucune des deux versions n'est installée).

from collections import Counter
a = Counter(0,1,2,1)
b = Counter(0,1,1)

print a - b  # ignores items in b missing in a

# check every element in a is in b
# a[key] returns 0 if key not in a, instead of raising an exception
assert all(a[key] > b[key] for key in b)

Modifier:

Puisque vous êtes bloqué avec la version 2.5, vous pouvez essayer de l’importer et définir votre propre version si cela échoue. De cette façon, vous serez sûr d'obtenir la dernière version si elle est disponible et de revenir à une version de travail si ce n'est pas le cas. Vous bénéficierez également d'améliorations de la vitesse si, le cas échéant, est converti en implémentation C à l'avenir.

c'est à dire.

try:
   from collections import Counter
except ImportError:
    class Counter(dict):
       ...

Vous pouvez trouver la source Python actuelle ici

29
Dave Kirby

Je sais que "pour" n’est pas ce que vous voulez, mais c’est simple et clair:

for x in b:
  a.remove(x)

Ou si les membres de b peuvent ne pas être dans a, utilisez:

for x in b:
  if x in a:
    a.remove(x)
54
Dyno Fu

Je ne suis pas sûr de ce que l'objection à une boucle for est: il n'y a pas de multiset dans Python, vous ne pouvez donc pas utiliser un conteneur intégré pour vous aider.

Il me semble que tout sur une ligne (si possible) sera probablement complexe à comprendre. Optez pour la lisibilité et KISS. Python n'est pas C :)

6
jkp

Python 2.7+ et 3.0 ont collections.Counter (a.k.a. multiset). La documentation est reliée à Recette 576611: Classe de compteur pour Python 2.5:

from operator import itemgetter
from heapq import nlargest
from itertools import repeat, ifilter

class Counter(dict):
    '''Dict subclass for counting hashable objects.  Sometimes called a bag
    or multiset.  Elements are stored as dictionary keys and their counts
    are stored as dictionary values.

    >>> Counter('zyzygy')
    Counter({'y': 3, 'z': 2, 'g': 1})

    '''

    def __init__(self, iterable=None, **kwds):
        '''Create a new, empty Counter object.  And if given, count elements
        from an input iterable.  Or, initialize the count from another mapping
        of elements to their counts.

        >>> c = Counter()                           # a new, empty counter
        >>> c = Counter('gallahad')                 # a new counter from an iterable
        >>> c = Counter({'a': 4, 'b': 2})           # a new counter from a mapping
        >>> c = Counter(a=4, b=2)                   # a new counter from keyword args

        '''        
        self.update(iterable, **kwds)

    def __missing__(self, key):
        return 0

    def most_common(self, n=None):
        '''List the n most common elements and their counts from the most
        common to the least.  If n is None, then list all element counts.

        >>> Counter('abracadabra').most_common(3)
        [('a', 5), ('r', 2), ('b', 2)]

        '''        
        if n is None:
            return sorted(self.iteritems(), key=itemgetter(1), reverse=True)
        return nlargest(n, self.iteritems(), key=itemgetter(1))

    def elements(self):
        '''Iterator over elements repeating each as many times as its count.

        >>> c = Counter('ABCABC')
        >>> sorted(c.elements())
        ['A', 'A', 'B', 'B', 'C', 'C']

        If an element's count has been set to zero or is a negative number,
        elements() will ignore it.

        '''
        for elem, count in self.iteritems():
            for _ in repeat(None, count):
                yield elem

    # Override dict methods where the meaning changes for Counter objects.

    @classmethod
    def fromkeys(cls, iterable, v=None):
        raise NotImplementedError(
            'Counter.fromkeys() is undefined.  Use Counter(iterable) instead.')

    def update(self, iterable=None, **kwds):
        '''Like dict.update() but add counts instead of replacing them.

        Source can be an iterable, a dictionary, or another Counter instance.

        >>> c = Counter('which')
        >>> c.update('witch')           # add elements from another iterable
        >>> d = Counter('watch')
        >>> c.update(d)                 # add elements from another counter
        >>> c['h']                      # four 'h' in which, witch, and watch
        4

        '''        
        if iterable is not None:
            if hasattr(iterable, 'iteritems'):
                if self:
                    self_get = self.get
                    for elem, count in iterable.iteritems():
                        self[elem] = self_get(elem, 0) + count
                else:
                    dict.update(self, iterable) # fast path when counter is empty
            else:
                self_get = self.get
                for elem in iterable:
                    self[elem] = self_get(elem, 0) + 1
        if kwds:
            self.update(kwds)

    def copy(self):
        'Like dict.copy() but returns a Counter instance instead of a dict.'
        return Counter(self)

    def __delitem__(self, elem):
        'Like dict.__delitem__() but does not raise KeyError for missing values.'
        if elem in self:
            dict.__delitem__(self, elem)

    def __repr__(self):
        if not self:
            return '%s()' % self.__class__.__name__
        items = ', '.join(map('%r: %r'.__mod__, self.most_common()))
        return '%s({%s})' % (self.__class__.__name__, items)

    # Multiset-style mathematical operations discussed in:
    #       Knuth TAOCP Volume II section 4.6.3 exercise 19
    #       and at http://en.wikipedia.org/wiki/Multiset
    #
    # Outputs guaranteed to only include positive counts.
    #
    # To strip negative and zero counts, add-in an empty counter:
    #       c += Counter()

    def __add__(self, other):
        '''Add counts from two counters.

        >>> Counter('abbb') + Counter('bcc')
        Counter({'b': 4, 'c': 2, 'a': 1})


        '''
        if not isinstance(other, Counter):
            return NotImplemented
        result = Counter()
        for elem in set(self) | set(other):
            newcount = self[elem] + other[elem]
            if newcount > 0:
                result[elem] = newcount
        return result

    def __sub__(self, other):
        ''' Subtract count, but keep only results with positive counts.

        >>> Counter('abbbc') - Counter('bccd')
        Counter({'b': 2, 'a': 1})

        '''
        if not isinstance(other, Counter):
            return NotImplemented
        result = Counter()
        for elem in set(self) | set(other):
            newcount = self[elem] - other[elem]
            if newcount > 0:
                result[elem] = newcount
        return result

    def __or__(self, other):
        '''Union is the maximum of value in either of the input counters.

        >>> Counter('abbb') | Counter('bcc')
        Counter({'b': 3, 'c': 2, 'a': 1})

        '''
        if not isinstance(other, Counter):
            return NotImplemented
        _max = max
        result = Counter()
        for elem in set(self) | set(other):
            newcount = _max(self[elem], other[elem])
            if newcount > 0:
                result[elem] = newcount
        return result

    def __and__(self, other):
        ''' Intersection is the minimum of corresponding counts.

        >>> Counter('abbb') & Counter('bcc')
        Counter({'b': 1})

        '''
        if not isinstance(other, Counter):
            return NotImplemented
        _min = min
        result = Counter()
        if len(self) < len(other):
            self, other = other, self
        for elem in ifilter(self.__contains__, other):
            newcount = _min(self[elem], other[elem])
            if newcount > 0:
                result[elem] = newcount
        return result


if __== '__main__':
    import doctest
    print doctest.testmod()

Ensuite, vous pouvez écrire

 a = Counter([0,1,2,1,0])
 b = Counter([0, 1, 1])
 c = a - b
 print list(c.elements())  # [0, 2]
5
ephemient

utiliser la compréhension de liste:

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

ferait l'affaire. Cela changerait cependant dans le processus. Mais je suis d’accord avec jkp et Dyno Fu pour dire qu’utiliser une boucle for serait préférable.

Peut-être que quelqu'un peut créer un meilleur exemple qui utilise la compréhension de liste mais reste KISS? 

4
BlackShift

Pour prouver le point de jkp que «tout ce qui est sur une ligne sera probablement complexe à comprendre», j'ai créé un one-liner. S'il vous plaît ne me modifiez pas parce que je comprends que ce n'est pas une solution que vous devriez réellement utiliser. C'est juste à des fins de démonstration.

L'idée est d'ajouter les valeurs une par une, tant que le total de fois que vous avez ajouté cette valeur est inférieur au nombre total de fois que cette valeur est dans un moins le nombre de fois qu'elle est dans b:

[ value for counter,value in enumerate(a) if a.count(value) >= b.count(value) + a[counter:].count(value) ]

L'horreur! Mais peut-être que quelqu'un peut l'améliorer? Est-ce même sans bug?

Edit: voyant Devin Jeanpierre commenter l’utilisation d’une structure de données à base de dictionnaire, j’ai imaginé cet oneliner:

sum([ [value]*count for value,count in {value:a.count(value)-b.count(value) for value in set(a)}.items() ], [])

Mieux, mais toujours illisible.

2
BlackShift

Voici une solution relativement longue mais efficace et lisible. C'est en marche).

def list_diff(list1, list2):
    counts = {}
    for x in list1:
        try:
            counts[x] += 1
        except:
            counts[x] = 1
    for x in list2:
        try:
            counts[x] -= 1
            if counts[x] < 0:
                raise ValueError('All elements of list2 not in list2')
        except:
            raise ValueError('All elements of list2 not in list1') 
    result = []
    for k, v in counts.iteritems():
        result += v*[k] 
    return result

a = [0, 1, 1, 2, 0]
b = [0, 1, 1]
%timeit list_diff(a, b)
%timeit list_diff(1000*a, 1000*b)
%timeit list_diff(1000000*a, 1000000*b)
100000 loops, best of 3: 4.8 µs per loop
1000 loops, best of 3: 1.18 ms per loop
1 loops, best of 3: 1.21 s per loop
0
JoeCondron

Vous pouvez utiliser la construction map pour le faire. Cela semble assez correct, mais sachez que la ligne map renverra elle-même une liste de Nones.

a = [1, 2, 3]
b = [2, 3]

map(lambda x:a.remove(x), b)
a
0
Bex

Vous pouvez essayer quelque chose comme ça:

class mylist(list):

    def __sub__(self, b):
        result = self[:]
        b = b[:]
        while b:
            try:
                result.remove(b.pop())
            except ValueError:
                raise Exception("Not all elements found during subtraction")
        return result


a = mylist([0, 1, 2, 1, 0] )
b = mylist([0, 1, 1])

>>> a - b
[2, 0]

Vous devez définir ce que [1, 2, 3] - [5, 6] doit générer, bien que, je suppose que vous souhaitiez [1, 2, 3], c’est pourquoi j’ignore l’erreur de valeur.

Edit: Maintenant, je vois que vous vouliez une exception si a ne contient pas tous les éléments, l'ajoutée au lieu de transmettre ValueError.

0
truppo

J'ai essayé de trouver une solution plus élégante, mais le mieux que j'ai pu faire était fondamentalement la même chose que Dyno Fu a dite:

from copy import copy

def subtract_lists(a, b):
    """
    >>> a = [0, 1, 2, 1, 0]
    >>> b = [0, 1, 1]
    >>> subtract_lists(a, b)
    [2, 0]

    >>> import random
    >>> size = 10000
    >>> a = [random.randrange(100) for _ in range(size)]
    >>> b = [random.randrange(100) for _ in range(size)]
    >>> c = subtract_lists(a, b)
    >>> assert all((x in a) for x in c)
    """
    a = copy(a)
    for x in b:
        if x in a:
            a.remove(x)
    return a
0
Christian Oudard