Se l'utilizzo di un pacchetto di terze parti va bene, è possibile utilizzare 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 l'ordine dell'elenco originale e ut può anche gestire elementi non lavabili come dizionari ricadendo su un algoritmo più lento ( O(n*m)
dove n
sono invece gli elementi nell'elenco originale e m
gli elementi univoci nell'elenco originale O(n)
). Nel caso in cui sia le chiavi che i valori siano hash, è possibile utilizzare l' key
argomento di quella funzione per creare elementi hash per il "test di unicità" (in modo che funzioni O(n)
).
Nel caso di un dizionario (che confronta indipendentemente dall'ordine) è necessario mapparlo su un'altra struttura di dati che confronta in questo modo, ad esempio frozenset
:
>>> list(unique_everseen(l, key=lambda item: frozenset(item.items())))
[{'a': 123}, {'b': 123}]
Nota che non dovresti usare un tuple
approccio semplice (senza ordinamento) perché dizionari uguali non hanno necessariamente lo stesso ordine (anche in Python 3.7 dove l'ordine di inserimento - non l'ordine assoluto - è garantito):
>>> d1 = {1: 1, 9: 9}
>>> d2 = {9: 9, 1: 1}
>>> d1 == d2
True
>>> tuple(d1.items()) == tuple(d2.items())
False
E anche l'ordinamento della tupla potrebbe non funzionare se le chiavi non sono ordinabili:
>>> d3 = {1: 1, 'a': 'a'}
>>> tuple(sorted(d3.items()))
TypeError: '<' not supported between instances of 'str' and 'int'
Prova delle prestazioni
Ho pensato che potesse essere utile vedere come si confrontano le prestazioni di questi approcci, quindi ho fatto un piccolo benchmark. I grafici di riferimento sono il tempo rispetto alle dimensioni dell'elenco in base a un elenco che non contiene duplicati (che è stato scelto arbitrariamente, il tempo di esecuzione non cambia in modo significativo se aggiungo alcuni o molti duplicati). È un diagramma log-log quindi viene coperto l'intero intervallo.
I tempi assoluti:
I tempi relativi all'approccio più veloce:
Il secondo approccio da thequourtheye è più veloce qui. L' unique_everseen
approccio con la key
funzione è al secondo posto, tuttavia è l'approccio più veloce a preservare l'ordine. Gli altri approcci di jcollado e thequourtheye sono quasi altrettanto veloci. L'approccio che utilizza unique_everseen
senza chiave e le soluzioni di Emmanuel e Scorpil sono molto lenti per elenchi più lunghi e si comportano molto peggio O(n*n)
invece che O(n)
. L' approccio di stpk con json
non lo è O(n*n)
ma è molto più lento di O(n)
approcci simili .
Il codice per riprodurre i benchmark:
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)
Per completezza ecco i tempi per un elenco contenente solo duplicati:
# 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)}
I tempi non cambiano in modo significativo se non unique_everseen
senza key
funzione, che in questo caso è la soluzione più veloce. Tuttavia, questo è solo il caso migliore (quindi non rappresentativo) per quella funzione con valori non lavabili perché il suo runtime dipende dalla quantità di valori univoci nell'elenco: O(n*m)
che in questo caso è solo 1 e quindi viene eseguito O(n)
.
Disclaimer: sono l'autore di iteration_utilities
.