2009-06-23 23 views
14

Recibo un diccionario como entrada y deseo devolver una lista de claves para las cuales los valores del diccionario son únicos en el alcance de ese diccionario.Python: encontrar claves con valores únicos en un diccionario?

Lo aclararé con un ejemplo. Di mi entrada es un diccionario, construido de la siguiente manera:

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 

El resultado que espero es ['dog', 'snake'].

Existen formas obvias de fuerza bruta para lograr esto, sin embargo, me pregunto si hay una forma clara de Python para hacer el trabajo.

Respuesta

12

Creo manera eficiente si dict es demasiado grande sería

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

Sería más lindo con collections.defaultdict (int), IMO –

+0

sí, pero lo dejaría para que la gente sepa lo que solíamos hacer cuando no había defaultdicts –

+0

WASTEFUL: does 'for k, v in a.iteritems (): 'pero no usa k !!! –

5

Tenga en cuenta que esto realmente es una fuerza bruta:

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

que no provocará [ 'perro', 'serpiente'] –

+0

¿No l.count ('perro') cero? Soy [3, 3, 2, 1, 4, 5, 1, 5] en mi sistema. –

+0

bien, veo que cobbal ya ha corregido el código. Gracias. –

-1

Se podría hacer algo como esto (simplemente contar el número de ocurrencias de cada valor):

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

Esto arroja valores (2, 4) cuando debería arrojar claves ('perro', 'serpiente'). –

+1

Me parece que 'defaultdict (int)' es un poco más claro que 'defaultdict (lambda: 0)'. Dado que un dict predeterminado de casi cualquier otro tipo simplemente usará el nombre del tipo. –

+0

Ah, dando un valor equivocado, lo siento. –

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) también funcionará –

+0

@Ryan: verdadero pero 'lambda: 0' es más explícito que' int' ... AFAICT, hasta que llegó el defaultdict [2.5], el número de personas que sabían que int() producía 0 [desde 2.2] en lugar de una excepción era

-2

¡Usa listas anidadas de comprensión!

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

No veo cómo usar las listas de comprensión de esta manera es una victoria. Para mí, esto solo hace que la solución sea más difícil de comprender (sin juego de palabras). La legibilidad es clave y esta solución no es tan fácil de leer IMO. –

+0

Rax pidió "una forma clara de Python para hacer el trabajo", en lugar de soluciones "obvias" para un problema que de otra manera sería trivial. –

+0

(1) Use 'k en a' en lugar de' k en a.keys() '(2) Use' whatever.itervalues ​​() 'en lugar de' whatever.values ​​() '(3) The dict (yadda yadda) parte está construyendo el ya excesivamente inverso inverso de 'a' ineficientemente (4) No es ni limpio ni Python (ic | ian) ... ¡pero ciertamente no es obvio! (5) Cuente el número de respondedores cuyo primer esfuerzo en el llamado problema trivial fue un relleno. –

0

Aquí hay otra variación.

>>> import collections 
>>> inverse= collections.defaultdict(list) 
>>> for k,v in a.items(): 
...  inverse[v].append(k) 
... 
>>> [ v[0] for v in inverse.values() if len(v) == 1 ] 
['dog', 'snake'] 

Soy parcial a esto porque el diccionario invertido es un patrón de diseño tan común.

+0

Desea [v [0] para k, v ...] en la última línea para obtener ['dog', 'snake'] según lo solicitado. –

+0

(1) En lugar de .items(), use .iteritems(). (2) La última línea extrae la clave innecesariamente; debería ser '[v [0] para v en inverse.itervalues ​​() si len (v) == 1' (3) En cualquier caso, construir todo el dict invertido es overkill. –

5

Aquí es una solución que sólo requiere que atraviesa el dict una vez:

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 se produce un valor 3 veces, intentará eliminar un elemento inexistente del 'resultado' ... docs say" "" remove (elem) Eliminar elementos elem del conjunto. Aumenta KeyError si elem no está contenido en el conjunto. "" " –

+0

¡Lo tiene! Lo he corregido para usar descarte() en su lugar. –

2

Un poco más detallado, pero sí necesita sólo un pase a:

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 ] 

(espero que funcione, ya que no puedo probarlo aquí)

+1

No funciona si una de las claves del diccionario es Ninguna. Por ejemplo, si a es {Ninguno: 1}, la salida debe ser [Ninguno], pero el código anterior producirá []. Además, 'x no es None' es preferible a' x! = None'. –

+0

¡Gracias por el comentario! Tienes toda la razón. En la práctica, rara vez ocurre que se use None ... pero incluso entonces, uno podría crear DummyObject: "Dummy = object()" en lugar de usar None. – Juergen

2

¿Qué pasa con la creación de subclases?

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

Esto tiene la ventaja de una menor huella de memoria, pero termina haciendo una O (N) búsqueda cada vez que configura un elemento, por lo que es probable que sea mucho más lento que el método de tabulación del diccionario. Además, creo que podrías usar un set para _inverse en lugar de dict. –

+0

Otro problema: el OP no impuso restricciones sobre cómo se obtuvieron los contenidos del dict. Entonces uno esperaría que 'del a ['bat']; imprimir a.unique_values ​​() 'haría que 'aardvark' apareciera en la salida, pero lamentablemente no funciona, y eso requeriría aún más convoluciones y __double__underscores__ :-( –

Cuestiones relacionadas