Rimuovi il dict duplicato nell'elenco in Python


153

Ho un elenco di dadi e vorrei rimuovere i dadi con coppie identiche di chiave e valore.

Per questo elenco: [{'a': 123}, {'b': 123}, {'a': 123}]

Vorrei restituire questo: [{'a': 123}, {'b': 123}]

Un altro esempio:

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

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


Puoi dirci di più sull'effettivo problema che stai cercando di risolvere? Sembra uno strano problema.
gfortune,

Sto combinando alcuni elenchi di dadi e ci sono duplicati. Quindi ho bisogno di rimuovere quei duplicati.
Brenden,

Ho trovato una soluzione in stackoverflow.com/questions/480214/… in una risposta senza l'utilizzo diset()
Sebastian Wagner,

Risposte:


242

Prova questo:

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

La strategia è convertire l'elenco dei dizionari in un elenco di tuple in cui le tuple contengono gli elementi del dizionario. Dato che è possibile eseguire l'hashing delle tuple, è possibile rimuovere i duplicati usando set(usando una comprensione set qui, l'alternativa più antica a Python sarebbe set(tuple(d.items()) for d in l)) e, successivamente, ricreare i dizionari dalle tuple con dict.

dove:

  • l è l'elenco originale
  • d è uno dei dizionari nell'elenco
  • t è una delle tuple create da un dizionario

Modifica: se si desidera conservare l'ordinamento, il one-liner sopra non funzionerà poiché setnon lo farà. Tuttavia, con alcune righe di codice, puoi anche farlo:

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

Esempio di output:

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

Nota: come sottolineato da @alexis, è possibile che due dizionari con le stesse chiavi e valori, non generino la stessa tupla. Ciò potrebbe accadere se passano attraverso una diversa aggiunta / rimozione della cronologia delle chiavi. In tal caso, considera l'ordinamento d.items()come suggerisce.


35
Bella soluzione ma ha un bug: d.items()non è garantito il ritorno di elementi in un ordine particolare. Dovresti tuple(sorted(d.items()))assicurarti di non avere tuple diverse per le stesse coppie chiave-valore.
alexis,

@alexis Ho fatto alcuni test e hai davvero ragione. Se molte chiavi vengono aggiunte tra e rimosse in un secondo momento, allora potrebbe essere il caso. Grazie mille per il tuo commento.
jcollado,

Freddo. Ho aggiunto la correzione alla tua risposta a beneficio dei futuri lettori che potrebbero non leggere l'intera conversazione.
alexis,

2
Nota, questo non funzionerà se carichi quell'elenco di dadi da un jsonmodulo come ho fatto io
Dhruv Ghulati,

2
Questa è una soluzione valida in questo caso, ma non funzionerà in caso di dizionari nidificati
Lorenzo Belli,

51

Un altro one-liner basato sulla comprensione dell'elenco:

>>> 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}]

Qui poiché possiamo usare il dictconfronto, manteniamo solo gli elementi che non sono nel resto dell'elenco iniziale (questa nozione è accessibile solo attraverso l'indice n, quindi l'uso di enumerate).


2
Questo funziona anche per un elenco di dizionari costituiti da elenchi rispetto alla prima risposta
gbozee,

1
questo funziona anche quando potresti avere un tipo non lavabile come valore nei tuoi dizionari, a differenza della risposta migliore.
Steve Rossiter,

1
qui, lo scopo è quello di rimuovere i valori duplicati, non la chiave, vedere il codice di questa risposta
Jamil Noyda

Questo è un codice molto inefficiente. if i not in d[n + 1:]scorre l'intero elenco di dicts (da nma che dimezza il numero totale di operazioni) e lo stai facendo per verificare ogni elemento nel tuo dizionario, quindi questo codice è O (n ^ 2) complessità temporale
Boris

non funziona per i dizionari con dizionari come valori
Roko Mijic,

22

Altre risposte non funzionerebbero se operi su dizionari nidificati come oggetti JSON deserializzati. Per questo caso è possibile utilizzare:

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]

1
Grande! il trucco è che l'oggetto dict non può essere aggiunto direttamente a un set, deve essere convertito in oggetto json da dump ().
Reihan_amn

19

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 nsono invece gli elementi nell'elenco originale e mgli elementi univoci nell'elenco originale O(n)). Nel caso in cui sia le chiavi che i valori siano hash, è possibile utilizzare l' keyargomento 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 tupleapproccio 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:

inserisci qui la descrizione dell'immagine

I tempi relativi all'approccio più veloce:

inserisci qui la descrizione dell'immagine

Il secondo approccio da thequourtheye è più veloce qui. L' unique_everseenapproccio con la keyfunzione è 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_everseensenza 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 jsonnon 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)}

inserisci qui la descrizione dell'immagine

I tempi non cambiano in modo significativo se non unique_everseensenza keyfunzione, 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.


15

A volte i loop vecchio stile sono ancora utili. Questo codice è leggermente più lungo di quello di jcollado, ma molto facile da leggere:

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])

L' 0in range(0, len(a))non è necessario.
Juan Antonio,

12

Se vuoi preservare l'Ordine, allora puoi farlo

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

Se l'ordine non ha importanza, puoi farlo

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

Nota: in Python 3, il tuo secondo approccio fornisce un dict_valuesoutput non serializzabile anziché un elenco. Devi lanciare nuovamente tutto in un elenco. list(frozen.....)
saran3h

12

Se stai usando Panda nel tuo flusso di lavoro, un'opzione è alimentare un elenco di dizionari direttamente al pd.DataFramecostruttore. Quindi utilizzare drop_duplicatese to_dictmetodi per il risultato richiesto.

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}]

3

Non una risposta universale , ma se la tua lista sembra essere ordinata per chiave, in questo modo:

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}]

quindi la soluzione è semplice come:

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

Risultato:

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

Funziona con dizionari nidificati e (ovviamente) conserva l'ordine.


1

Puoi usare un set, ma devi trasformare i dadi in un tipo hash.

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)

Unico ora è uguale

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

Per recuperare i dicts:

[dict(x) for x in unique]

L'ordine di d.iteritems()non è garantito, quindi potresti finire con "duplicati" in unique.
danodonovan,

-1

Ecco una rapida soluzione a una riga con una comprensione dell'elenco doppiamente annidata (basata sulla soluzione di @Emmanuel).

Questo utilizza una chiave singola (ad esempio, a) in ogni dict come chiave primaria, anziché verificare se l'intero dict corrisponde

[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:]]]

Non è ciò che OP ha richiesto, ma è ciò che mi ha portato a questa discussione, quindi ho pensato di pubblicare la soluzione con cui sono finito


-1

Non così breve ma facile da leggere:

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)

Ora, l'elenco list_of_data_uniqavrà dicts unici.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.