2009-06-23 7 views
14

Je reçois un dictionnaire en entrée et je souhaite renvoyer une liste de clés pour lesquelles les valeurs du dictionnaire sont uniques dans la portée de ce dictionnaire.Python: trouver des clés avec des valeurs uniques dans un dictionnaire?

Je vais clarifier avec un exemple. Dire que mon entrée est le dictionnaire a, construit comme suit:

a = dict() 
a['cat'] =  1 
a['fish'] =  1 
a['dog'] =  2 # <-- unique 
a['bat'] =  3 
a['aardvark'] = 3 
a['snake'] = 4 # <-- unique 
a['wallaby'] = 5 
a['badger'] = 5 

Le résultat est que j'attends ['dog', 'snake'].

Il existe des moyens de force brute évidents pour y parvenir, mais je me demandais s'il y avait une manière propre Pythonian pour faire le travail.

Répondre

12

Je pense de manière efficace si dict est trop grande serait

countMap = {} 
for v in a.itervalues(): 
    countMap[v] = countMap.get(v,0) + 1 
uni = [ k for k, v in a.iteritems() if countMap[v] == 1] 
+1

Ce serait plus joli avec collections.defaultdict (int), IMO –

+0

oui, mais je le laisserais pour que les gens sachent ce que nous faisons quand il n'y a pas de defaultdicts –

+0

WASTEFUL: fait 'pour k, v dans a.iteritems (): 'mais n'utilise pas k !!! –

5

Notez que ceci est en fait une force brute:

l = a.values() 
b = [x for x in a if l.count(a[x]) == 1] 
+0

il ne sera pas sortie [ 'chien', 'serpent'] –

+0

est pas l.count ('chien') zéro? l est [3, 3, 2, 1, 4, 5, 1, 5] sur mon système. –

+0

ok, je vois que cobbal a déjà corrigé le code. Merci. –

-1

Vous pourriez faire quelque chose comme ça (il suffit de compter le nombre d'occurrences pour chaque valeur):

def unique(a): 
    from collections import defaultdict 
    count = defaultdict(lambda: 0) 
    for k, v in a.iteritems(): 
     count[v] += 1 
    for v, c in count.iteritems(): 
     if c <= 1: 
      yield v 
+0

Ceci donne des valeurs (2, 4) quand il devrait donner des clés ('chien', 'serpent'). –

+1

Je trouve que 'defaultdict (int)' est un peu plus clair que 'defaultdict (lambda: 0)'. Puisqu'une dict par défaut de presque n'importe quel autre type utilisera simplement le nom de type. –

+0

Ah, ce qui donne une valeur erronée, désolé. –

4
>>> b = [] 
>>> import collections 
>>> bag = collections.defaultdict(lambda: 0) 
>>> for v in a.itervalues(): 
...  bag[v] += 1 
... 
>>> b = [k for (k, v) in a.iteritems() if bag[v] == 1] 
>>> b.sort() # optional 
>>> print b 
['dog', 'snake'] 
>>> 
+0

collections.defaultdict (int) fonctionnera aussi –

+0

@Ryan: Vrai mais 'lambda: 0' est plus explicite que' int' ... AFAICT, jusqu'à ce que defaultdict arrive [2.5], le nombre de personnes qui savaient que int() produisait 0 [depuis 2.2] à la place d'une exception était

-2

Utilisez les listes de lecture imbriquées!

print [v[0] for v in 
      dict([(v, [k for k in a.keys() if a[k] == v]) 
        for v in set(a.values())]).values() 
     if len(v) == 1] 
+1

Je ne vois pas comment l'utilisation de la liste de compréhension de cette manière est une victoire. Pour moi, cela rend la solution plus difficile à comprendre (sans jeu de mots). La lisibilité est la clé et cette solution n'est tout simplement pas lisible par l'OMI. –

+0

Rax a demandé "une manière propre à Python de faire le travail", par opposition à des solutions "évidentes" à un problème par ailleurs trivial. –

+0

(1) Utilisez 'k in a' au lieu de' k dans a.keys() '(2) Utilisez' whatever.itervalues ​​() 'au lieu de' whatever.values ​​() '(3) Le dict (yadda yadda) la partie est en train de construire l'inverse déjà surpeuplé de 'a' inefficacement (4) Ce n'est ni propre ni Python (ic | ian) ... mais ce n'est certainement pas évident! (5) Compter le nombre de répondeurs dont le premier effort sur le problème dit trivial était un trucage. –

0

Voici une autre variante. Je suis partial à cela parce que le dictionnaire inversé est un modèle de conception si commun.

+0

Vous voulez [v [0] pour k, v ...] dans la dernière ligne pour obtenir ['dog', 'snake'] comme demandé. –

+0

(1) Au lieu de .items(), utilisez .iteritems(). (2) La dernière ligne extrait la clé inutilement; devrait être '[v [0] pour v dans inverse.itervalues ​​() si len (v) == 1' (3) Dans tous les cas, construire tout le dict inversé est trop puissant. –

5

Voici une solution qui ne nécessite traverser une fois que le dict:

def unique_values(d): 
    seen = {} # dict (value, key) 
    result = set() # keys with unique values 
    for k,v in d.iteritems(): 
     if v in seen: 
      result.discard(seen[v]) 
     else: 
      seen[v] = k 
      result.add(k) 
    return list(result) 
+0

Si une valeur survient 3 fois, vous essaierez de supprimer un élément inexistant de 'result' ... docs dit" "" remove (elem) Supprime l'élément elem de l'ensemble. Déclenche KeyError si elem n'est pas contenu dans l'ensemble. "" " –

+0

Exactement! Je l'ai corrigé pour utiliser discard() à la place. –

2

Un peu plus bavard, mais il a besoin qu'une seule passe sur un:

revDict = {} 
for k, v in a.iteritems(): 
    if v in revDict: 
    revDict[v] = None 
    else: 
    revDict[v] = k 

[ x for x in revDict.itervalues() if x != None ] 

(je l'espère cela fonctionne, puisque je ne peux pas le tester ici)

+1

Ne fonctionne pas si l'une des clés du dictionnaire est Aucune. Par exemple, si a est {None: 1}, la sortie doit être [None] mais le code ci-dessus produira []. Aussi: 'x is not None' est préférable à' x! = None'. –

+0

Merci pour le commentaire! Tu as complètement raison. Dans la pratique, il arrive rarement que None soit utilisé ... mais même alors, on pourrait créer un objet DummyObject: "Dummy = object()" au lieu d'utiliser None. – Juergen

2

Qu'en est-il de sous-classement?

class UniqueValuesDict(dict): 

    def __init__(self, *args): 
     dict.__init__(self, *args) 
     self._inverse = {} 

    def __setitem__(self, key, value): 
     if value in self.values(): 
      if value in self._inverse: 
       del self._inverse[value] 
     else: 
      self._inverse[value] = key 
     dict.__setitem__(self, key, value) 

    def unique_values(self): 
     return self._inverse.values() 

a = UniqueValuesDict() 

a['cat'] =  1 
a['fish'] =  1 
a[None] =  1 
a['duck'] =  1 
a['dog'] =  2 # <-- unique 
a['bat'] =  3 
a['aardvark'] = 3 
a['snake'] = 4 # <-- unique 
a['wallaby'] = 5 
a['badger'] = 5 

assert a.unique_values() == ['dog', 'snake'] 
+0

Ceci a l'avantage d'avoir une empreinte mémoire plus petite, mais vous finissez par faire une recherche O (N) chaque fois que vous définissez un élément, donc il est susceptible d'être beaucoup plus lent que la méthode de tabulation de dictionnaire. En outre, je pense que vous pourriez utiliser un ensemble pour _inverse au lieu d'un dict. –

+0

Un autre problème: l'OP n'a imposé aucune contrainte sur la façon dont le contenu de la dict a été obtenu. On s'attend donc à ce que 'del a ['bat']; print a.unique_values ​​() 'ferait que' aardvark' apparaisse dans la sortie mais malheureusement il ne le fait pas et la fixation nécessiterait encore plus de convolutions et __double__underscores__ :-( –

Questions connexes