Si l'utilisation d'un package tiers vous convient, vous pouvez utiliser 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}]
Il préserve l'ordre de la liste d'origine et ut peut également gérer les éléments non phasables comme les dictionnaires en recourant à un algorithme plus lent ( O(n*m)
où n
sont les éléments de la liste d'origine et m
les éléments uniques de la liste d'origine au lieu de O(n)
). Dans le cas où les clés et les valeurs sont hachables, vous pouvez utiliser l' key
argument de cette fonction pour créer des éléments hachables pour le "test d'unicité" (afin qu'il fonctionne O(n)
).
Dans le cas d'un dictionnaire (qui compare indépendamment de l'ordre), vous devez le mapper à une autre structure de données qui compare comme ça, par exemple frozenset
:
>>> list(unique_everseen(l, key=lambda item: frozenset(item.items())))
[{'a': 123}, {'b': 123}]
Notez que vous ne devez pas utiliser une tuple
approche simple (sans tri) car les dictionnaires égaux n'ont pas nécessairement le même ordre (même en Python 3.7 où l' ordre d'insertion - et non l'ordre absolu - est garanti):
>>> d1 = {1: 1, 9: 9}
>>> d2 = {9: 9, 1: 1}
>>> d1 == d2
True
>>> tuple(d1.items()) == tuple(d2.items())
False
Et même le tri du tuple peut ne pas fonctionner si les clés ne peuvent pas être triées:
>>> d3 = {1: 1, 'a': 'a'}
>>> tuple(sorted(d3.items()))
TypeError: '<' not supported between instances of 'str' and 'int'
Référence
J'ai pensé qu'il pourrait être utile de voir comment les performances de ces approches se comparent, alors j'ai fait un petit benchmark. Les graphiques de référence sont le temps par rapport à la taille de la liste sur la base d'une liste ne contenant aucun doublon (qui a été choisi arbitrairement, le temps d'exécution ne change pas de manière significative si j'ajoute certains ou beaucoup de doublons). C'est un tracé log-log donc la gamme complète est couverte.
Les temps absolus:
Les horaires relatifs à l'approche la plus rapide:
La deuxième approche de thefourtheye est la plus rapide ici. L' unique_everseen
approche avec la key
fonction est à la deuxième place, mais c'est l'approche la plus rapide qui préserve l'ordre. Les autres approches de jcollado et thefourtheye sont presque aussi rapides. L'approche utilisant unique_everseen
sans clé et les solutions d' Emmanuel et Scorpil sont très lentes pour les listes plus longues et se comportent bien plus mal O(n*n)
au lieu de O(n)
. L' approche de stpk avec json
n'est pas, O(n*n)
mais elle est beaucoup plus lente que les O(n)
approches similaires .
Le code pour reproduire les 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)
Par souci d'exhaustivité, voici le timing d'une liste contenant uniquement des doublons:
# 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)}
Les horaires ne changent pas de manière significative, sauf unique_everseen
sans key
fonction, ce qui dans ce cas est la solution la plus rapide. Cependant, ce n'est que le meilleur cas (donc non représentatif) pour cette fonction avec des valeurs non phasables car son exécution dépend de la quantité de valeurs uniques dans la liste: O(n*m)
qui dans ce cas est juste 1 et donc elle s'exécute O(n)
.
Avertissement: je suis l'auteur de iteration_utilities
.