Удалить дубликат dict в списке в Python
У меня есть список dicts, и я хотел бы удалить dicts с идентичными парами ключей и значений.
Для этого списка: [{'a': 123}, {'b': 123}, {'a': 123}]
Я хотел бы вернуть это: [{'a': 123}, {'b': 123}]
Другой пример:
Для этого списка: [{'a': 123, 'b': 1234}, {'a': 3222, 'b': 1234}, {'a': 123, 'b': 1234}]
Я хотел бы вернуть это: [{'a': 123, 'b': 1234}, {'a': 3222, 'b': 1234}]
Ответы
Ответ 1
Попробуй это:
[dict(t) for t in {tuple(d.items()) for d in l}]
Стратегия заключается в преобразовании списка словарей в список кортежей, в которых кортежи содержат элементы словаря. Поскольку кортежи можно хэшировать, вы можете удалить дубликаты, используя set
(используя здесь понимание набора, будет set(tuple(d.items()) for d in l)
более старая альтернатива python set(tuple(d.items()) for d in l)
), и после этого создайте заново словари из кортежей с dict
.
где:
-
l
оригинальный список -
d
является одним из словарей в списке -
t
является одним из кортежей, созданных из словаря
Редактировать: Если вы хотите сохранить порядок, однострочная строка выше не будет работать, так как set
не сделает этого. Однако, с помощью нескольких строк кода, вы также можете сделать это:
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
Пример вывода:
[{'a': 123, 'b': 1234}, {'a': 3222, 'b': 1234}]
Примечание. Как указывает @alexis, два словаря с одинаковыми ключами и значениями могут не давать один и тот же кортеж. Это может произойти, если они пройдут через другую историю добавления/удаления ключей. Если это так для вашей проблемы, то рассмотрите сортировку d.items()
как он предлагает.
Ответ 2
Еще один однострочный шрифт, основанный на понимании списка:
>>> 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}]
Здесь, поскольку мы можем использовать сравнение dict
, мы сохраняем только элементы, которые не находятся в остальной части исходного списка (это понятие доступно только через индекс n
, следовательно, использование enumerate
).
Ответ 3
Иногда старые циклы все еще полезны. Этот код немного длиннее jcollado, но очень легко читается:
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])
Ответ 4
Другие ответы не будут работать, если вы работаете с вложенными словарями, такими как десериализованные объекты JSON. Для этого случая вы можете использовать:
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]
Ответ 5
Если вы хотите сохранить заказ, вы можете сделать
from collections import OrderedDict
print OrderedDict((frozenset(item.items()),item) for item in data).values()
# [{'a': 123, 'b': 1234}, {'a': 3222, 'b': 1234}]
Если порядок не имеет значения, вы можете сделать
print {frozenset(item.items()):item for item in data}.values()
# [{'a': 3222, 'b': 1234}, {'a': 123, 'b': 1234}]
Ответ 6
Если вы можете использовать сторонний пакет, то вы можете использовать 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]()
Сроки относительно самого быстрого подхода:
![enter image description here]()
Второй подход из четвертого здесь самый быстрый. Подход unique_everseen
с функцией key
находится на втором месте, однако это самый быстрый подход, сохраняющий порядок. Другие подходы от jcollado и thefourtheye почти такие же быстрые. Подход с использованием unique_everseen
без ключа и решений от Emmanuel и 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]()
unique_everseen
не изменяются значительно, за исключением unique_everseen
без key
функции, которая в этом случае является самым быстрым решением. Тем не менее, это как раз лучший вариант (поэтому не репрезентативный) для этой функции с неисчерпаемыми значениями, потому что время ее выполнения зависит от количества уникальных значений в списке: O(n*m)
которое в данном случае равно только 1 и, следовательно, выполняется в O(n)
.
Отказ от ответственности: я являюсь автором iteration_utilities
.
Ответ 7
Если вы используете Pandas в своем рабочем процессе, одним из вариантов является подача списка словарей непосредственно в конструктор pd.DataFrame
. Затем используйте drop_duplicates
и to_dict
для требуемого результата.
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}]
Ответ 8
Не универсальный ответ, но если ваш список отсортирован по некоторому ключу, например так:
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}]
тогда решение так же просто, как:
import itertools
result = [a[0] for a in itertools.groupby(l)]
Результат:
[{'a': {'b': 31}, 't': 1},
{'a': {'b': 145}, 't': 2},
{'a': {'b': 25231}, 't': 2},
{'a': {'b': 112}, 't': 3}]
Работает с вложенными словарями и (очевидно) сохраняет порядок.
Ответ 9
Вы можете использовать набор, но вам нужно превратить dicts в хешируемый тип.
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)
Уникальный теперь равен
set([(('a', 3222), ('b', 1234)), (('a', 123), ('b', 1234))])
Чтобы получить ответ:
[dict(x) for x in unique]