Видаліть дублікат дикту зі списку в Python


153

У мене є список діктів, і я хотів би видалити дикти з однаковими парами ключів і значень.

Для цього списку: [{'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}]


Чи можете ви розповісти більше про актуальну проблему, яку ви намагаєтеся вирішити? Це здається незвичайною проблемою.
gfortune

Я поєдную кілька списків диктів і є дублікати. Тому мені потрібно видалити ці дублікати.
Бренден

Я знайшов рішення в stackoverflow.com/questions/480214/… у відповіді без використанняset()
Себастьян Вагнер

Відповіді:


242

Спробуйте це:

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

Стратегія полягає в перетворенні списку словників до списку кортежів, де кортежі містять елементи словника. Оскільки кортежі можуть бути хешировані, ви можете видалити дублікати, використовуючи set(використовуючи тут розуміння набору , буде старшою альтернативою 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()як він пропонує.


35
Приємне рішення, але в ньому є помилка: d.items()не гарантується повернення елементів у певному порядку. Ви повинні зробити так, tuple(sorted(d.items()))щоб не отримували різні кортежі для одних і тих же пар ключових значень.
alexis

@alexis Я зробив кілька тестів, і ти справді маєш рацію. Якщо багато ключів буде додано між ними та видалено пізніше, то це може бути так. Дякую за ваш коментар.
jcollado

Класно. Я додав виправлення до вашої відповіді на користь майбутніх читачів, які, можливо, не прочитали всю розмову.
alexis

2
Зауважте, це не спрацює, якщо ви завантажите у цей список диктів з jsonмодуля, як я
Dhruv Ghulati

2
Це дійсне рішення в цьому випадку, але не спрацює у випадку вкладених словників
Лоренцо Беллі

51

Ще один однокласник, заснований на розумінні списку:

>>> 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).


2
Це також працює для списку словників, які складаються зі списків порівняно з першою відповіддю
gbozee

1
це також працює, коли у ваших словниках може бути незмінний тип як значення, на відміну від верхньої відповіді.
Стів Россітер

1
тут, мета - видалити повторювані значення, а не ключові, дивіться код цієї відповіді
Джаміль Нойда

Це дуже неефективний код. if i not in d[n + 1:]ітераціює над усім списком диктів (з-за того, nщо лише вдвічі зменшує загальну кількість операцій), і ви робите це для перевірки кожного елемента у вашому словнику, тому цей код є часовою складністю O (n ^ 2)
Борис

не працює для словників зі словниками як цінностями
Roko Mijic

22

Інші відповіді не працюватимуть, якщо ви працюєте над вкладеними словниками, такими як деріаріалізовані об'єкти 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]

1
Чудово! хитрість полягає в тому, що об’єкт dict не може бути безпосередньо доданий до набору, його потрібно перетворити на об'єкт json dump ().
Reihan_amn

18

Якщо використання сторонніх пакетів було б добре, ви можете використовувати 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'

Орієнтир

Я вважав, що може бути корисним побачити, як порівнюється ефективність цих підходів, тому я зробив невеликий орієнтир. Графіки орієнтування - це час та розмір списку на основі списку, що не містить дублікатів (це було вибрано довільно; час виконання не зміниться суттєво, якщо я додаю кілька або багато дублікатів). Це графік журналу журналу, тому весь діапазон охоплюється.

Абсолютні часи:

введіть тут опис зображення

Терміни відносно найшвидшого підходу:

введіть тут опис зображення

Тут найшвидший другий підхід з боку очей . unique_everseenПідхід з keyфункцією знаходиться на другому місці, однак це найшвидший підхід , який зберігає замовлення. Інші підходи від jcollado і thefourtheye майже так само швидко. Підхід із використанням unique_everseenбез ключа та рішень Еммануїла та Скорпіля дуже повільний для більш довгих списків і поводиться набагато гірше 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)}

введіть тут опис зображення

Терміни не змінюються суттєво, крім unique_everseenkey функції без функції, яка в цьому випадку є найшвидшим рішенням. Однак це лише найкращий випадок (настільки не репрезентативний) для цієї функції з незмінними значеннями, оскільки час її виконання залежить від кількості унікальних значень у списку: O(n*m)що в цьому випадку становить лише 1, і, таким чином, воно працює O(n).


Відмова: Я автор автора iteration_utilities.


15

Іноді петлі старого стилю все ще корисні. Цей код трохи довший, ніж 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])

0У range(0, len(a))не потрібно.
Хуан Антоніо

12

Якщо ви хочете зберегти Орден, то можете зробити

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

Примітка: у python 3 ваш другий підхід дає не серійний dict_valuesвихід замість списку. Ви повинні знову занести все до списку. list(frozen.....)
saran3h

12

Якщо ви використовуєте 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}]

3

Не є універсальною відповіддю , але якщо ваш список буде відсортований за деяким ключем, наприклад, такий:

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

Працює з вкладеними словниками і (очевидно) зберігає порядок.


1

Ви можете використовувати набір, але вам потрібно перетворити дикти в тип хешируемого.

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]

Порядок d.iteritems()не гарантований - ви можете отримати "копії" в unique.
danodonovan

-1

Ось швидке однолінійне рішення з розумінням списку удвічі вкладеного (на основі рішення @Emmanuel).

При цьому використовується один ключ (наприклад, a) у кожному диктаті в якості основного ключа, а не перевірка відповідності всього дикту

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

Це не те, про що просив ОП, але це те, що мене наштовхнуло на цю тему, тож я зрозумів, що опублікую рішення, з яким я закінчився


-1

Не такий короткий, але легкий для читання:

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)

Тепер у списку list_of_data_uniqбудуть унікальні дикти.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.