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 n
están los elementos en la lista original y m
los 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 key
argumento 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 tuple
enfoque 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:
Los tiempos relativos al enfoque más rápido:
El segundo enfoque de thefourtheye es más rápido aquí. El unique_everseen
enfoque con la key
funció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_everseen
sin 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 json
no 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)}
Los tiempos no cambian significativamente, excepto unique_everseen
sin la key
funció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
.