Python – resta una lista de dicts de otro

Me interesa comparar listas múltiples, tomar la diferencia e iterar a través de eso.

Ambas son listas de diccionarios que contienen las siguientes claves: ‘ssid’ – str, ‘bssid’ – str, ‘channel’ – int, ‘flags’ – list, ‘found’ – bool

He intentado:

list = list(set(networks_list).difference(missing_networks)) 

Pero recibo el error:

 unhashable type 'dict' 

Mi estructura de datos se ve así:

 list: [{'found': False, 'flags': ['WPA2-PSK-CCMP', 'WPS', 'ESS'], 'ssid': 'SOHO_BROADCAST', 'bssid': '30:46:9a:9d:11:1a', 'channel': 1}, {'found': False, 'flags': ['WPA-EAP-TKIP', 'WPA2-EAP-CCMP', 'ESS'], 'ssid': 'Cisco 2.4ghz', 'bssid': '40:f4:ec:7f:3c:5a', 'channel': 11}, {'found': False, 'flags': ['WPA-EAP-TKIP', 'WPA2-EAP-CCMP', 'ESS'], 'ssid': 'Cisco 5.0ghz', 'bssid': '40:f4:ec:7f:3c:54', 'channel': 149}] 

Las redes faltantes están inicialmente vacías.

¿Hay una forma sencilla de hacer esto?

Probablemente haya muchos escollos en un enfoque genérico como este, pero si sus diccionarios son en su mayoría primitivos, y no son enormes, puede hacer algo como esto:

Suponiendo que sus datos se parecen a esto:

 networks = [ {'address': '192.168.1.1'}, {'address': '127.0.0.1'}, ] missing = [ {'address': '127.0.0.1'} ] 

Puede convertir las listas de diccionarios en listas de tuplas (que son hashable)

 def make_hashable(d): return (frozenset(x.iteritems()) for x in d) networks_hashable = make_hashable(networks) missing_hashable = make_hashable(missing) 

Luego restar

 diff = set(networks_hashable).difference(missing_hashable) 

Ahora tienes una lista de tuplas.

 print list(diff) 

o, convertir de nuevo a diccionarios

 print [dict(x) for x in diff] 

Actualizar

He cambiado la definición de make_hashable basada en el comentario de @gnibbler’s.

En lugar de hacer una lista de los dictados, haga una lista de objetos que implementen __eq__ y __hash__ y el código que proporcione debería funcionar

Un dict es un objeto mutable. Esto significa que no tiene un valor hash constante a lo largo de su vida, y no se puede poner en un conjunto.

Si convierte todos los dictados en cadenas con la misma función, se convierten en hashable y puede usarlos en un conjunto …

No, en general es bastante difícil de hacer eficientemente. Sin embargo, no tiene que resolver el caso general, solo para su estructura de datos particular que no nos ha elaborado.

Por ejemplo, si las teclas dict son todas int o str , es considerablemente más fácil que si las teclas son números complejos, etc.

EDITAR: Ya que nos ha dicho su estructura de datos, le puedo decir que una forma sencilla es convertir los dictados en nombres .

Nota: no puede simplemente convertir el dict en una tupla con tuple(dict.items() ) porque el orden de las claves puede ser diferente de un dict a otro

 >>> d = dict(ssid="ssid", bssid="bssid", channel=1, flags="flags", found="True") >>> networks_list = [d, ] >>> from collections import namedtuple >>> NT = namedtuple("my_struct", d.keys()) >>> set(NT(**i) for i in networks_list) set([my_struct(found='True', flags='flags', channel=1, bssid='bssid', ssid='ssid')]) 

¿Qué pasa si intentas algo tan simple como:

  lst = list(set(networks_list.items()).difference(set(missing_networks.items()))) 

(Por cierto: he cambiado su variable nombrada a lst aquí; vincular algunos resultados al nombre “lista” es probablemente una mala idea, dado que Python admite una función list() . No es una palabra clave, por lo que no arrojará una excepción, pero podría tropezar con él más adelante cuando escriba algún código que intente llamar a la función list() más adelante).

Este enfoque funciona:

 >>> import random >>> items = [{'ssid': 'foo%s' % i, 'bssid': 'bar%s' % i, 'channel': i, 'flags': 'abc%s' % i, 'found': random.choice([True, False])} for i in range(1, 11)] >>> items1 = random.sample(items, 7) >>> items2 = random.sample(items, 5) >>> print "\n".join(map(str, items1)) {'found': True, 'flags': 'abc9', 'ssid': 'foo9', 'bssid': 'bar9', 'channel': 9} {'found': True, 'flags': 'abc7', 'ssid': 'foo7', 'bssid': 'bar7', 'channel': 7} {'found': False, 'flags': 'abc10', 'ssid': 'foo10', 'bssid': 'bar10', 'channel': 10} {'found': True, 'flags': 'abc5', 'ssid': 'foo5', 'bssid': 'bar5', 'channel': 5} {'found': False, 'flags': 'abc4', 'ssid': 'foo4', 'bssid': 'bar4', 'channel': 4} {'found': True, 'flags': 'abc3', 'ssid': 'foo3', 'bssid': 'bar3', 'channel': 3} {'found': True, 'flags': 'abc2', 'ssid': 'foo2', 'bssid': 'bar2', 'channel': 2} >>> print "\n".join(map(str, items2)) {'found': True, 'flags': 'abc3', 'ssid': 'foo3', 'bssid': 'bar3', 'channel': 3} {'found': True, 'flags': 'abc9', 'ssid': 'foo9', 'bssid': 'bar9', 'channel': 9} {'found': False, 'flags': 'abc1', 'ssid': 'foo1', 'bssid': 'bar1', 'channel': 1} {'found': False, 'flags': 'abc8', 'ssid': 'foo8', 'bssid': 'bar8', 'channel': 8} {'found': True, 'flags': 'abc5', 'ssid': 'foo5', 'bssid': 'bar5', 'channel': 5} >>> print "\n".join(map(str, [dict(itemset) for itemset in set([tuple(sorted(grp.items())) for grp in items1]).difference([tuple(sorted(grp.items())) for grp in items2])])) {'found': False, 'flags': 'abc10', 'ssid': 'foo10', 'bssid': 'bar10', 'channel': 10} {'found': False, 'flags': 'abc4', 'ssid': 'foo4', 'bssid': 'bar4', 'channel': 4} {'found': True, 'flags': 'abc7', 'ssid': 'foo7', 'bssid': 'bar7', 'channel': 7} {'found': True, 'flags': 'abc2', 'ssid': 'foo2', 'bssid': 'bar2', 'channel': 2} 

Como se mencionó anteriormente, los dictados son mutables y, por lo tanto, no pueden ser operados por set () – eso es porque no hay garantía de que una vez colocados dentro de un conjunto, un dict no cambie y se vuelva igual a otro elemento existente del conjunto, por lo tanto violando la calidad del set.

Si solo está verificando la igualdad de los dicts, puede convertirlos en tuplas, luego usar las tuplas en las operaciones set (), y luego convertir las tuplas en el conjunto resultante nuevamente en dicts.

 >>> d = {1:1, 2:2} >>> t = tuple(d1.items()) >>> t ((1, 1), (2, 2)) >>> d_ = dict(t) >>> d_ {1: 1, 2: 2} >>> d == d_ True 

Envolver los dictados en clases puede ser un poco más engorroso, ya que todavía tiene que resolver la conversión de un dictado a un tipo de datos inmutables.

Como tienes listas dentro de tus dictados, tienes más trabajo. Lo más simple es si pudiera reemplazar listas con tuplas en los dados originales.

Suponiendo que eso no sea factible, su proceso de conversión tendrá que ser una función, en lugar de solo llamar a tuple () y dict (), respectivamente. Primero deberá convertir las listas en tuplas, luego convierta los dados en tuplas en lugar de listas en tuplas. Por ejemplo:

 >>> d = {'int1': 1, 'int2': 2, 'list1': ['a', 'b'], 'list2': ['x', 'y']} >>> d_l = {} >>> for key, value in d.iteritems(): ... if type(value) == list: ... d_l[key] = tuple(value) ... else: ... d_l[key] = value >>> d_l {'int1': 1, 'int2': 2, 'list1': ('a', 'b'), 'list2': ('x', 'y')} >>> d_ = tuple(d_l.iteritems()) >>> d_ (('int1', 1), ('int2', 2), ('list1', ('a', 'b')), ('list2', ('x', 'y'))) 

Para volver a convertir, tienes dos opciones. Mire los valores clave que sabe que corresponden a listas (si sus claves son conocidas y no cambian) o mire las tuplas donde el segundo elemento es una tupla en sí misma (de usted no almacena ninguna tupla en los dados originales). Si ninguna de las dos opciones se aplica, debe escribir algoritmos de conversión más complejos.

Use una lista de comprensión:

 >>> l1 = [{1:1, 'a':2},{1:2, 'a':4},{1:5, 'a':'2'}] >>> l2 = [{1:1, 'a':3},{1:2, 'a':4},{1:5, 'a':'t'}] >>> l3 = [i for i in l1 if i not in l2] >>> l3 [{'a': 2, 1: 1}, {'a': '2', 1: 5}] 

Voy a ir por la respuesta de Eric aquí.

Primero, el problema en cuestión. ¿Por qué un dictado es inhalable? En pocas palabras, porque es un contenedor mutable. Si cambias el contenido del dict, el hash cambia. Lo mismo sucederá con cualquier otro contenedor mutable como listas. Entonces, tienes que usar algo que es inmutable.

La solución más simple en mi mente sería usar una clase de envoltorio. Esencialmente, una clase que tiene una sola propiedad es el dictado que originalmente querías. Puedes condimentarlo con cualquier función mágica que desees para las comparaciones.

Por lo tanto, si tengo su lista original de redes

 network_list = [ {'found': False, 'flags': ['WPA2-PSK-CCMP', 'WPS', 'ESS'], 'ssid': 'SOHO_BROADCAST', 'bssid': '30:46:9a:9d:11:1a', 'channel': 1}, {'found': False, 'flags': ['WPA-EAP-TKIP', 'WPA2-EAP-CCMP', 'ESS'], 'ssid': 'Cisco 2.4ghz', 'bssid': '40:f4:ec:7f:3c:5a', 'channel': 11}, {'found': False, 'flags': ['WPA-EAP-TKIP', 'WPA2-EAP-CCMP', 'ESS'], 'ssid': 'Cisco 5.0ghz', 'bssid': '40:f4:ec:7f:3c:54', 'channel': 149} ] 

Puedo aplicar fácilmente una clase de envoltorio.

 class Wrapper(object): def __init__(self, **kwargs): for key, value in kwargs.items(): setattr(self, key, value) wrapped_networks = [Wrapper(**{'net_dict': network}) for network in network_list] 

De esa manera, el diccionario se almacena y se puede acceder a través de

 wrapped_networks[0].net_dict # etc... 

o cualquier otra cosa que puedas desear nombrarla. Además, debido a la forma en que se implementa la clase, puedes usarla para envolver lo que quieras, ¡incluso si hay varias cosas por Envoltorio!

Lo que esto hace, como debe saber, es que lo que realmente se está procesando es el objeto, de acuerdo con una ID única asignada en el tiempo de ejecución. Un poco de refactorización para sus funciones de diferencia para trabajar con estos envoltorios, y usted debe estar bien encaminado (a menos que encuentre una solución mejor = D)