Remover dict duplicado na lista em Python

153

Eu tenho uma lista de dictos e gostaria de removê-los com pares de chave e valor idênticos.

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

Queria devolver isto: [{'a': 123}, {'b': 123}]

Outro exemplo:

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

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

Brenden
fonte
Você pode nos contar mais sobre o problema real que está tentando resolver? Este parece ser um problema estranho.
gfortune
Estou combinando algumas listas de ditados e há duplicatas. Então, eu preciso remover essas duplicatas.
Brenden
Eu encontrei uma solução em stackoverflow.com/questions/480214/... em uma resposta sem o uso deset()
Sebastian Wagner

Respostas:

242

Tente o seguinte:

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

A estratégia é converter a lista de dicionários em uma lista de tuplas onde as tuplas contêm os itens do dicionário. Como as tuplas podem ser hash, você pode remover duplicatas usando set(usando uma compreensão de conjunto aqui, seria a alternativa mais antiga do python set(tuple(d.items()) for d in l)) e, depois disso, recrie os dicionários das tuplas com dict.

Onde:

  • l é a lista original
  • d é um dos dicionários da lista
  • t é uma das tuplas criadas a partir de um dicionário

Editar: se você deseja preservar os pedidos, a linha única acima não funcionará, pois setnão fará isso. No entanto, com algumas linhas de código, você também pode fazer isso:

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

Exemplo de saída:

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

Nota: Conforme apontado pelo @alexis, pode acontecer que dois dicionários com as mesmas chaves e valores não resultem na mesma tupla. Isso pode acontecer se eles passarem por um histórico diferente de adicionar / remover chaves. Se esse for o caso do seu problema, considere a classificação d.items()como ele sugere.

jcollado
fonte
35
Solução agradável, mas possui um bug: d.items()não é garantido o retorno de elementos em uma ordem específica. Você deve fazer isso tuple(sorted(d.items()))para garantir que não obtenha tuplas diferentes para os mesmos pares de valores-chave.
24412 alexis
@alexis Fiz alguns testes e você está realmente certo. Se muitas chaves forem adicionadas e removidas posteriormente, esse poderá ser o caso. Muito obrigado pelo seu comentário.
jcollado
Legal. Adicionei a correção à sua resposta para o benefício de futuros leitores que talvez não leiam a conversa toda.
24412 alexis
2
Note, isso não vai funcionar se você carregar na lista de dicts de um a jsonmódulo como eu fiz
Dhruv Ghulati
2
Esta é uma solução válida neste caso, mas não funcionará no caso de dicionários aninhados
Lorenzo Belli
51

Outra linha de base baseada na compreensão da 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}]

Aqui, como podemos usar a dictcomparação, mantemos apenas os elementos que não estão no restante da lista inicial (essa noção é acessível apenas através do índice n, daí o uso de enumerate).

Emmanuel
fonte
2
Isso também funciona para uma lista de dicionários que consistem em listas, em comparação a primeira resposta
gbozee
1
isso também funciona quando você pode ter um tipo lavável como valor em seus dicionários, ao contrário da resposta principal.
Steve Rossiter
1
aqui, o objetivo é remover valores duplicados, não chave, ver o código desta resposta
Jamil Noyda
Este é um código muito ineficiente. if i not in d[n + 1:]itera sobre a lista inteira de dicts (de n, mas que apenas metades do número total de operações) e que está fazendo essa verificação para cada elemento no seu dicionário de modo que este este código é O (n ^ 2) complexidade de tempo
Boris
não funciona para dicionários com dicionários como valores
Roko Mijic 04/06
22

Outras respostas não funcionariam se você estiver operando em dicionários aninhados, como objetos JSON desserializados. Para este caso, você pode 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
fonte
1
Ótimo! o truque é que o objeto dict não pode ser adicionado diretamente a um conjunto, ele precisa ser convertido no objeto json por dump ().
Reihan_amn
19

Se o uso de um pacote de terceiros estiver correto, você poderá 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}]

Ele preserva a ordem da lista original e ut também pode manipular itens laváveis ​​como dicionários, recorrendo a um algoritmo mais lento ( O(n*m)onde nestão os elementos na lista original e mos elementos únicos na lista original O(n)). No caso de chaves e valores serem hashable, você pode usar o keyargumento dessa função para criar itens hashable para o "teste de exclusividade" (para que funcione O(n)).

No caso de um dicionário (que compara independentemente da ordem), você precisa mapeá-lo para outra estrutura de dados que se compara assim, por exemplo frozenset:

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

Observe que você não deve usar uma tupleabordagem simples (sem classificação) porque dicionários iguais não necessariamente têm a mesma ordem (mesmo no Python 3.7 onde a ordem de inserção - não a ordem absoluta - é garantida):

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

E mesmo classificar a tupla pode não funcionar se as chaves não forem classificáveis:

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

Referência

Eu pensei que seria útil ver como o desempenho dessas abordagens se compara, então fiz uma pequena referência. Os gráficos de referência são o tempo versus o tamanho da lista com base em uma lista que não contém duplicatas (que foi escolhida arbitrariamente, o tempo de execução não muda significativamente se eu adicionar algumas ou muitas duplicatas). É um gráfico de log-log, para que toda a gama seja coberta.

Os tempos absolutos:

insira a descrição da imagem aqui

Os tempos relativos à abordagem mais rápida:

insira a descrição da imagem aqui

A segunda abordagem do quarto olho é mais rápida aqui. A unique_everseenabordagem com a keyfunção está em segundo lugar, no entanto, é a abordagem mais rápida que preserva a ordem. As outras abordagens de jcollado e thefourtheye são quase tão rápidas. A abordagem usando unique_everseensem chave e as soluções de Emmanuel e Scorpil são muito lentas para listas mais longas e se comportam muito pior em O(n*n)vez de O(n). A abordagem do stpkjson não é, O(n*n)mas é muito mais lenta que as O(n)abordagens semelhantes .

O código para reproduzir os benchmarks:

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, é o momento para uma lista que contém apenas duplicatas:

# 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)}

insira a descrição da imagem aqui

Os tempos não mudam significativamente, exceto unique_everseensem keyfunção, que neste caso é a solução mais rápida. No entanto, esse é apenas o melhor caso (não representativo) para essa função com valores laváveis, porque o tempo de execução depende da quantidade de valores exclusivos da lista: O(n*m)que neste caso é apenas 1 e, portanto, é executada O(n).


Disclaimer: Eu sou o autor de iteration_utilities.

MSeifert
fonte
15

Às vezes, loops de estilo antigo ainda são úteis. Este código é um pouco mais longo que o do jcollado, mas é muito fácil de ler:

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
fonte
O 0no range(0, len(a))não é necessário.
Juan Antonio
12

Se você deseja preservar a Ordem, pode fazer

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

Se o pedido não importa, você pode fazer

print {frozenset(item.items()):item for item in data}.values()
# [{'a': 3222, 'b': 1234}, {'a': 123, 'b': 1234}]
thefourtheye
fonte
Nota: no python 3, sua segunda abordagem fornece uma dict_valuessaída não serializável em vez de uma lista. Você precisa colocar a coisa toda em uma lista novamente. list(frozen.....)
saran3h
12

Se você estiver usando o Pandas no seu fluxo de trabalho, uma opção é alimentar uma lista de dicionários diretamente ao pd.DataFrameconstrutor. Em seguida, use drop_duplicatese to_dictmétodos para o resultado desejado.

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
fonte
3

Não é uma resposta universal , mas se sua lista for classificada por alguma chave, desta forma:

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

então a solução é tão simples quanto:

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 com dicionários aninhados e (obviamente) preserva a ordem.

Highstaker
fonte
1

Você pode usar um conjunto, mas precisa transformar os dict em um tipo lavável.

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 agora é igual

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

Para recuperar os ditados:

[dict(x) for x in unique]
Matimus
fonte
A ordem de d.iteritems()não é garantida; portanto, você poderá acabar com 'duplicatas' unique.
danodonovan 02/10/19
-1

Aqui está uma solução rápida de uma linha com uma compreensão de lista duplamente aninhada (com base na solução de @Emmanuel).

Isso usa uma única chave (por exemplo a) em cada ditado como chave primária, em vez de verificar se o ditado inteiro corresponde

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

Não é o que o OP pediu, mas foi o que me levou a esse segmento, então pensei em publicar a solução com a qual acabei

Alec
fonte
-1

Não é tão curto, mas fácil de ler:

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)

Agora, a lista list_of_data_uniqterá ditados únicos.

user1723157
fonte