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 n
estão os elementos na lista original e m
os elementos únicos na lista original O(n)
). No caso de chaves e valores serem hashable, você pode usar o key
argumento 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 tuple
abordagem 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:
Os tempos relativos à abordagem mais rápida:
A segunda abordagem do quarto olho é mais rápida aqui. A unique_everseen
abordagem com a key
funçã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_everseen
sem 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)}
Os tempos não mudam significativamente, exceto unique_everseen
sem key
funçã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
.