Если с использованием стороннего пакета все будет в порядке, вы можете использовать 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}]
Он сохраняет порядок исходного списка и ut также может обрабатывать небезупречныйтакие элементы, как словари, прибегая к более медленному алгоритму (O(n*m)
, где n
- элементы в исходном списке и m
уникальные элементы в исходном списке вместо O(n)
).Если ключи и значения могут быть хэшируемыми, вы можете использовать аргумент key
этой функции для создания хэшируемых элементов для «теста уникальности» (чтобы он работал в O(n)
).
В этом случаесловаря (который сравнивается независимо от порядка), вам нужно сопоставить его с другой структурой данных, которая сравнивается подобным образом, например frozenset
:
>>> list(unique_everseen(l, key=lambda item: frozenset(item.items())))
[{'a': 123}, {'b': 123}]
Обратите внимание, что вы не должны использовать простой tuple
подход (без сортировки), поскольку равные словари не обязательно имеют одинаковый порядок (даже в Python 3.7, где порядок вставки - не абсолютный порядок - гарантирован):
>>> d1 = {1: 1, 9: 9}
>>> d2 = {9: 9, 1: 1}
>>> d1 == d2
True
>>> tuple(d1.items()) == tuple(d2.items())
False
И даже сортировка кортежа может не сработать, если ключи не сортируются:
>>> d3 = {1: 1, 'a': 'a'}
>>> tuple(sorted(d3.items()))
TypeError: '<' not supported between instances of 'str' and 'int'
Тест
Я подумал, что было бы полезно посмотреть, как сравнивается производительность этих подходов, поэтому ясделал небольшой тест.Графики сравнения времени и размера списка основаны на списке, не содержащем дубликатов (который был выбран произвольно, время выполнения существенно не изменится, если я добавлю несколько или много дубликатов).Это логарифмический график, поэтому охватывается весь диапазон.
Абсолютное время:
![enter image description here](https://i.stack.imgur.com/KumMp.png)
Время относительно самого быстрогозаход на посадку:
![enter image description here](https://i.stack.imgur.com/7E4tH.png)
Второй подход с thefourtheye здесь самый быстрый.Подход unique_everseen
с функцией key
находится на втором месте, однако это самый быстрый подход, который сохраняет порядок.Другие подходы из jcollado и thefourtheye почти такие же быстрые.Подход с использованием unique_everseen
без ключа и решений от Эммануила и Scorpil очень медленен для длинных списков и ведет себя намного хуже O(n*n)
вместо O(n)
.Подход stpk с json
не O(n*n)
, но он намного медленнее аналогичных O(n)
подходов.
Код для воспроизведения эталонных тестов:
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)
Для полноты здесь приведено время для списка, содержащего только дубликаты:
# 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)}
![enter image description here](https://i.stack.imgur.com/WwLks.png)
Время не изменяется значительно, за исключением unique_everseen
без функции key
, которая в этом случае является самым быстрым решением.Тем не менее, это просто лучший случай (поэтому не репрезентативный) для этой функции с непредсказуемыми значениями, потому что ее время выполнения зависит от количества уникальных значений в списке: O(n*m)
, который в данном случае равен только 1 и, следовательно, работает в O(n)
.
Отказ от ответственности: я автор iteration_utilities
.