Eliminar duplicados dict en la lista en Python

153

Tengo una lista de dictados, y me gustaría eliminar los dictados con pares idénticos de clave y valor.

Para esta lista: [{'a': 123}, {'b': 123}, {'a': 123}]

Quisiera regresar esto: [{'a': 123}, {'b': 123}]

Otro ejemplo:

Para esta lista: [{'a': 123, 'b': 1234}, {'a': 3222, 'b': 1234}, {'a': 123, 'b': 1234}]

Quisiera regresar esto: [{'a': 123, 'b': 1234}, {'a': 3222, 'b': 1234}]

Brenden
fuente
¿Puede contarnos más sobre el problema real que está tratando de resolver? Esto parece un problema extraño de tener.
gfortune
Estoy combinando algunas listas de dictados y hay duplicados. Entonces necesito eliminar esos duplicados.
Brenden
Encontré una solución en stackoverflow.com/questions/480214/… en una respuesta sin el uso deset()
Sebastian Wagner

Respuestas:

242

Prueba esto:

[dict(t) for t in {tuple(d.items()) for d in l}]

La estrategia es convertir la lista de diccionarios en una lista de tuplas donde las tuplas contienen los elementos del diccionario. Dado que las tuplas se pueden dividir en hash, puede eliminar duplicados usando set(usando una comprensión establecida aquí, la alternativa más antigua de Python sería set(tuple(d.items()) for d in l)) y, después de eso, volver a crear los diccionarios a partir de tuplas condict .

dónde:

  • l es la lista original
  • d es uno de los diccionarios en la lista
  • t es una de las tuplas creadas a partir de un diccionario

Editar: si desea conservar el pedido, la línea anterior no funcionará, ya setque no lo hará. Sin embargo, con algunas líneas de código, también puede hacer eso:

l = [{'a': 123, 'b': 1234},
        {'a': 3222, 'b': 1234},
        {'a': 123, 'b': 1234}]

seen = set()
new_l = []
for d in l:
    t = tuple(d.items())
    if t not in seen:
        seen.add(t)
        new_l.append(d)

print new_l

Salida de ejemplo:

[{'a': 123, 'b': 1234}, {'a': 3222, 'b': 1234}]

Nota: Como señaló @alexis, puede suceder que dos diccionarios con las mismas claves y valores no generen la misma tupla. Eso podría suceder si pasan por un historial diferente de agregar / quitar claves. Si ese es el caso de su problema, considere ordenar d.items()como él sugiere.

jcollado
fuente
35
Buena solución pero tiene un error: d.items()no se garantiza que devuelva elementos en un orden particular. Debe hacer tuple(sorted(d.items()))para asegurarse de no obtener diferentes tuplas para los mismos pares clave-valor.
alexis
@alexis Hice algunas pruebas y tienes razón. Si se agregan muchas claves intermedias y se eliminan más tarde, entonces ese podría ser el caso. Muchas gracias por tu comentario.
jcollado
Frio. Agregué la solución a su respuesta para beneficio de futuros lectores que podrían no leer toda la conversación.
alexis
2
Tenga en cuenta que esto no funcionará si carga en esa lista de dictados del jsonmódulo como lo hice yo
Dhruv Ghulati
2
Esta es una solución válida en este caso, pero no funcionará en el caso de los diccionarios anidados
Lorenzo Belli
51

Otra frase basada en la comprensión de la lista:

>>> d = [{'a': 123}, {'b': 123}, {'a': 123}]
>>> [i for n, i in enumerate(d) if i not in d[n + 1:]]
[{'b': 123}, {'a': 123}]

Aquí, dado que podemos usar la dictcomparación, solo conservamos los elementos que no están en el resto de la lista inicial (esta noción solo es accesible a través del índice n, de ahí el uso de enumerate).

Emmanuel
fuente
2
Esto también funciona para una lista de diccionarios que consisten en listas en comparación con la primera respuesta
gbozee
1
esto también funciona cuando puede tener un tipo no compartible como valor en sus diccionarios, a diferencia de la respuesta principal.
Steve Rossiter
1
aquí, el propósito es eliminar valores duplicados, no clave, ver el código de esta respuesta
Jamil Noyda
Este es un código muy ineficiente. if i not in d[n + 1:]itera sobre la lista completa de dictados (de npero que solo reduce a la mitad el número total de operaciones) y está haciendo esa verificación para cada elemento en su diccionario, por lo que este código es O (n ^ 2) complejidad de tiempo
Boris
no funciona para diccionarios con diccionarios como valores
Roko Mijic
22

Otras respuestas no funcionarían si está operando en diccionarios anidados, como objetos JSON deserializados. Para este caso puedes usar:

import json
set_of_jsons = {json.dumps(d, sort_keys=True) for d in X}
X = [json.loads(t) for t in set_of_jsons]
stpk
fuente
1
¡Excelente! El truco es que el objeto dict no se puede agregar directamente a un conjunto, sino que debe convertirse en un objeto json mediante dump ().
Reihan_amn
19

Si usar un paquete de terceros estaría bien, entonces podría usar iteration_utilities.unique_everseen:

>>> from iteration_utilities import unique_everseen
>>> l = [{'a': 123}, {'b': 123}, {'a': 123}]
>>> list(unique_everseen(l))
[{'a': 123}, {'b': 123}]

Conserva el orden de la lista original y ut también puede manejar elementos no compartibles como los diccionarios recurriendo a un algoritmo más lento ( O(n*m)donde nestán los elementos en la lista original y mlos elementos únicos en la lista original en lugar de O(n)). En caso de que tanto las claves como los valores sean hashables, puede usar el keyargumento de esa función para crear elementos hashaable para la "prueba de unicidad" (para que funcione O(n)).

En el caso de un diccionario (que se compara independientemente del orden), debe asignarlo a otra estructura de datos que se compare así, por ejemplo frozenset:

>>> list(unique_everseen(l, key=lambda item: frozenset(item.items())))
[{'a': 123}, {'b': 123}]

Tenga en cuenta que no debe usar un tupleenfoque simple (sin ordenar) porque los diccionarios iguales no necesariamente tienen el mismo orden (incluso en Python 3.7, donde se garantiza el orden de inserción , no el orden absoluto):

>>> d1 = {1: 1, 9: 9}
>>> d2 = {9: 9, 1: 1}
>>> d1 == d2
True
>>> tuple(d1.items()) == tuple(d2.items())
False

E incluso ordenar la tupla podría no funcionar si las teclas no se pueden ordenar:

>>> d3 = {1: 1, 'a': 'a'}
>>> tuple(sorted(d3.items()))
TypeError: '<' not supported between instances of 'str' and 'int'

Punto de referencia

Pensé que podría ser útil ver cómo se compara el rendimiento de estos enfoques, así que hice un pequeño punto de referencia. Los gráficos de referencia son el tiempo frente al tamaño de la lista basado en una lista que no contiene duplicados (que se eligió arbitrariamente, el tiempo de ejecución no cambia significativamente si agrego algunos o muchos duplicados). Es un diagrama de registro de registro, por lo que se cubre el rango completo.

Los tiempos absolutos:

ingrese la descripción de la imagen aquí

Los tiempos relativos al enfoque más rápido:

ingrese la descripción de la imagen aquí

El segundo enfoque de thefourtheye es más rápido aquí. El unique_everseenenfoque con la keyfunción está en el segundo lugar, sin embargo, es el enfoque más rápido que conserva el orden. Los otros enfoques de jcollado y thefourtheye son casi tan rápido. El enfoque que usa unique_everseensin clave y las soluciones de Emmanuel y Scorpil son muy lentas para listas más largas y se comportan mucho peor en O(n*n)lugar de O(n). El enfoque de stpk con jsonno es O(n*n)pero es mucho más lento que los O(n)enfoques similares .

El código para reproducir los puntos de referencia:

from simple_benchmark import benchmark
import json
from collections import OrderedDict
from iteration_utilities import unique_everseen

def jcollado_1(l):
    return [dict(t) for t in {tuple(d.items()) for d in l}]

def jcollado_2(l):
    seen = set()
    new_l = []
    for d in l:
        t = tuple(d.items())
        if t not in seen:
            seen.add(t)
            new_l.append(d)
    return new_l

def Emmanuel(d):
    return [i for n, i in enumerate(d) if i not in d[n + 1:]]

def Scorpil(a):
    b = []
    for i in range(0, len(a)):
        if a[i] not in a[i+1:]:
            b.append(a[i])

def stpk(X):
    set_of_jsons = {json.dumps(d, sort_keys=True) for d in X}
    return [json.loads(t) for t in set_of_jsons]

def thefourtheye_1(data):
    return OrderedDict((frozenset(item.items()),item) for item in data).values()

def thefourtheye_2(data):
    return {frozenset(item.items()):item for item in data}.values()

def iu_1(l):
    return list(unique_everseen(l))

def iu_2(l):
    return list(unique_everseen(l, key=lambda inner_dict: frozenset(inner_dict.items())))

funcs = (jcollado_1, Emmanuel, stpk, Scorpil, thefourtheye_1, thefourtheye_2, iu_1, jcollado_2, iu_2)
arguments = {2**i: [{'a': j} for j in range(2**i)] for i in range(2, 12)}
b = benchmark(funcs, arguments, 'list size')

%matplotlib widget
import matplotlib as mpl
import matplotlib.pyplot as plt
plt.style.use('ggplot')
mpl.rcParams['figure.figsize'] = '8, 6'

b.plot(relative_to=thefourtheye_2)

Para completar, aquí está el momento para una lista que contiene solo duplicados:

# this is the only change for the benchmark
arguments = {2**i: [{'a': 1} for j in range(2**i)] for i in range(2, 12)}

ingrese la descripción de la imagen aquí

Los tiempos no cambian significativamente, excepto unique_everseensin la keyfunción, que en este caso es la solución más rápida. Sin embargo, ese es el mejor caso (por lo que no es representativo) para esa función con valores no compartibles porque su tiempo de ejecución depende de la cantidad de valores únicos en la lista: O(n*m)que en este caso es solo 1 y, por lo tanto, se ejecuta O(n).


Descargo de responsabilidad: soy el autor de iteration_utilities.

MSeifert
fuente
15

A veces, los bucles antiguos siguen siendo útiles. Este código es un poco más largo que el de jcollado, pero muy fácil de leer:

a = [{'a': 123}, {'b': 123}, {'a': 123}]
b = []
for i in range(0, len(a)):
    if a[i] not in a[i+1:]:
        b.append(a[i])
Scorpil
fuente
El 0en range(0, len(a))que no es necesario.
Juan Antonio
12

Si desea preservar la orden, puede hacerlo

from collections import OrderedDict
print OrderedDict((frozenset(item.items()),item) for item in data).values()
# [{'a': 123, 'b': 1234}, {'a': 3222, 'b': 1234}]

Si el orden no importa, entonces puedes hacerlo

print {frozenset(item.items()):item for item in data}.values()
# [{'a': 3222, 'b': 1234}, {'a': 123, 'b': 1234}]
thefourtheye
fuente
Nota: en Python 3, su segundo enfoque proporciona una dict_valuessalida no serializable en lugar de una lista. Tienes que volver a incluir todo en una lista. list(frozen.....)
saran3h
12

Si está utilizando Pandas en su flujo de trabajo, una opción es alimentar una lista de diccionarios directamente al pd.DataFrameconstructor. Luego use drop_duplicatesy to_dictmétodos para el resultado requerido.

import pandas as pd

d = [{'a': 123, 'b': 1234}, {'a': 3222, 'b': 1234}, {'a': 123, 'b': 1234}]

d_unique = pd.DataFrame(d).drop_duplicates().to_dict('records')

print(d_unique)

[{'a': 123, 'b': 1234}, {'a': 3222, 'b': 1234}]
jpp
fuente
3

No es una respuesta universal , pero si su lista está ordenada por alguna clave, como esta:

l=[{'a': {'b': 31}, 't': 1},
   {'a': {'b': 31}, 't': 1},
 {'a': {'b': 145}, 't': 2},
 {'a': {'b': 25231}, 't': 2},
 {'a': {'b': 25231}, 't': 2}, 
 {'a': {'b': 25231}, 't': 2}, 
 {'a': {'b': 112}, 't': 3}]

entonces la solución es tan simple como:

import itertools
result = [a[0] for a in itertools.groupby(l)]

Resultado:

[{'a': {'b': 31}, 't': 1},
{'a': {'b': 145}, 't': 2},
{'a': {'b': 25231}, 't': 2},
{'a': {'b': 112}, 't': 3}]

Funciona con diccionarios anidados y (obviamente) conserva el orden.

Highstaker
fuente
1

Puede usar un conjunto, pero debe convertir los dictos en un tipo hashable.

seq = [{'a': 123, 'b': 1234}, {'a': 3222, 'b': 1234}, {'a': 123, 'b': 1234}]
unique = set()
for d in seq:
    t = tuple(d.iteritems())
    unique.add(t)

Único ahora es igual

set([(('a', 3222), ('b', 1234)), (('a', 123), ('b', 1234))])

Para recuperar los dictados:

[dict(x) for x in unique]
Matimus
fuente
El orden de d.iteritems()no está garantizado, por lo que puede terminar con 'duplicados' unique.
danodonovan
-1

Aquí hay una solución rápida de una línea con una comprensión de lista doblemente anidada (basada en la solución de @Emmanuel).

Esto usa una sola clave (por ejemplo a) en cada dict como clave principal, en lugar de verificar si la dict completa coincide

[i for n, i in enumerate(list_of_dicts) if i.get(primary_key) not in [y.get(primary_key) for y in list_of_dicts[n + 1:]]]

No es lo que OP solicitó, pero es lo que me trajo a este hilo, así que pensé que publicaría la solución con la que terminé

Alec
fuente
-1

No tan corto pero fácil de leer:

list_of_data = [{'a': 123}, {'b': 123}, {'a': 123}]

list_of_data_uniq = []
for data in list_of_data:
    if data not in list_of_data_uniq:
        list_of_data_uniq.append(data)

Ahora, la lista list_of_data_uniqtendrá dictados únicos.

usuario1723157
fuente