Чтобы удалить дубликаты из списка словарей Python по одному ключу, обычно достаточно пройти список один раз, хранить уже встреченные значения ключа в set и добавлять в результат только первый словарь с новым значением. Такой подход сохраняет первый найденный элемент для каждого значения ключа и не изменяет исходный список.
Базовое решение: оставить первый словарь для каждого значения ключа
Пусть есть список пользователей, где поле id должно быть уникальным:
items = [
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"},
{"id": 1, "name": "Alice updated"},
{"id": 3, "name": "Carol"},
{"id": 2, "name": "Bob updated"},
]
Удаление повторов по id:
seen = set()
unique_items = []
for item in items:
key = item["id"]
if key not in seen:
seen.add(key)
unique_items.append(item)
В unique_items попадут словари с первым встреченным значением каждого id:
[
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"},
{"id": 3, "name": "Carol"},
]
Проверить результат можно обычным assert:
assert unique_items == [
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"},
{"id": 3, "name": "Carol"},
]
Почему используется set
В множество seen записываются значения ключа, которые уже встретились. Для каждого очередного словаря выполняются три действия:
- получить значение нужного поля;
- проверить, встречалось ли оно раньше;
- если нет — сохранить значение в
seenи сам словарь в результирующем списке.
Сам список словарей в множество помещать не нужно: словарь является изменяемым объектом и не подходит в качестве элемента set. В множество помещается только значение поля, по которому определяется уникальность.
Значение ключа, добавляемое вset, должно быть хешируемым. Строки, числа,Noneи кортежи из хешируемых объектов подходят. Списки и словари — нет.
Функция для повторного использования
Если операция нужна в нескольких местах, удобнее вынести её в функцию:
def unique_by_key(items, key):
seen = set()
result = []
for item in items:
value = item[key]
if value not in seen:
seen.add(value)
result.append(item)
return result
Использование:
users = unique_by_key(items, "id")
Проверка:
assert [item["id"] for item in users] == [1, 2, 3]
Функция предполагает, что каждый словарь содержит переданный ключ. Если хотя бы в одном элементе его нет, выражение item[key] вызовет KeyError. Это полезное поведение, когда отсутствие поля означает ошибку входных данных.
Что делать, если ключ может отсутствовать
Если неполные словари допустимы, сначала нужно определить правило для отсутствующего поля. Например, можно пропускать такие элементы:
def unique_by_existing_key(items, key):
seen = set()
result = []
for item in items:
if key not in item:
continue
value = item[key]
if value not in seen:
seen.add(value)
result.append(item)
return result
Другой вариант — считать отсутствие ключа отдельным значением. Для этого можно использовать уникальный объект-маркер:
missing = object()
seen = set()
result = []
for item in items:
value = item.get("id", missing)
if value not in seen:
seen.add(value)
result.append(item)
В таком случае среди словарей без id сохранится только первый. Использовать None как маркер не всегда правильно: None может быть допустимым реальным значением поля.
Если нужно оставить последнее вхождение
Иногда новый словарь должен заменять старый. Например, более поздняя запись с тем же id содержит обновлённые данные. Понятный вариант — хранить индекс уже добавленного элемента:
def unique_by_key_keep_last(items, key):
indexes = {}
result = []
for item in items:
value = item[key]
if value in indexes:
result[indexes[value]] = item
else:
indexes[value] = len(result)
result.append(item)
return result
Для исходного примера результат будет эквивалентен:
[
{"id": 1, "name": "Alice updated"},
{"id": 2, "name": "Bob updated"},
{"id": 3, "name": "Carol"},
]
Проверка:
result = unique_by_key_keep_last(items, "id")
assert result == [
{"id": 1, "name": "Alice updated"},
{"id": 2, "name": "Bob updated"},
{"id": 3, "name": "Carol"},
]
При таком варианте позиция элемента определяется его первым появлением, а содержимое заменяется при последующих совпадениях.
Короткий вариант через словарь
Если требуется оставить последнее значение для каждого ключа и отдельная логика обработки повторов не нужна, можно построить вспомогательный словарь:
unique_items = list({
item["id"]: item
for item in items
}.values())
При повторном присваивании одному ключу значение в словаре заменяется, поэтому для одинакового id останется последний соответствующий объект. Этот вариант короче, но явный цикл обычно удобнее, если нужно одновременно валидировать данные, считать дубликаты или выбирать между первым и последним элементом по дополнительному условию.
Удаление дублей по нескольким полям
Если уникальность определяется не одним полем, а комбинацией значений, в set можно помещать кортеж. Например, запись считается повтором только при совпадении first_name и last_name:
people = [
{"first_name": "Ivan", "last_name": "Petrov", "age": 30},
{"first_name": "Anna", "last_name": "Ivanova", "age": 25},
{"first_name": "Ivan", "last_name": "Petrov", "age": 31},
]
seen = set()
unique_people = []
for person in people:
key = (person["first_name"], person["last_name"])
if key not in seen:
seen.add(key)
unique_people.append(person)
В результате останется первая запись для каждой уникальной пары имени и фамилии.
Если значение ключа — список или словарь
Следующий код завершится ошибкой, если item["tags"] содержит список:
seen.add(item["tags"])
Причина в том, что список не является хешируемым. Если порядок элементов списка имеет значение и сами элементы хешируемы, его можно преобразовать в кортеж:
key = tuple(item["tags"])
Например:
items = [
{"id": 1, "tags": ["python", "api"]},
{"id": 2, "tags": ["python", "api"]},
{"id": 3, "tags": ["api", "python"]},
]
seen = set()
result = []
for item in items:
key = tuple(item["tags"])
if key not in seen:
seen.add(key)
result.append(item)
Здесь списки ["python", "api"] и ["api", "python"] считаются разными. Если порядок тегов не должен влиять на уникальность, необходимо отдельно нормализовать данные в соответствии с требованиями задачи. Простая сортировка подходит только тогда, когда элементы действительно допускают такое сравнение и изменение порядка не меняет смысл данных.
Не путать дублирование по ключу с полным равенством словарей
Задача «удалить одинаковые словари» отличается от задачи «оставить один словарь для каждого id». Например:
{"id": 1, "name": "Alice"}
{"id": 1, "name": "Alice updated"}
Словари отличаются, но по полю id являются дублями. Поэтому сначала нужно определить критерий уникальности, а затем строить ключ именно по нему.
Как посчитать удалённые дубликаты
Если дополнительно нужно узнать количество повторов, счётчик удобно добавить в тот же проход:
seen = set()
result = []
duplicates = 0
for item in items:
value = item["id"]
if value in seen:
duplicates += 1
continue
seen.add(value)
result.append(item)
Количество удалённых элементов можно проверить и после обработки:
assert duplicates == len(items) - len(result)
Такой вариант полезен при обработке импортированных данных: результат содержит уникальные записи, а счётчик позволяет отдельно контролировать количество отброшенных повторов.
Типичные ошибки
- Добавлять сам словарь в set. Обычный
dictнельзя использовать как элемент множества. - Использовать item.get без продуманного значения по умолчанию. Отсутствующий ключ и настоящее значение
Noneмогут случайно стать одной группой. - Не определить, какой дубль сохранять. Для одной задачи нужен первый элемент, для другой — последний.
- Использовать нехешируемое значение как ключ уникальности. Список или словарь необходимо сначала представить в подходящей неизменяемой форме, если это допустимо по смыслу данных.
- Удалять элементы из исходного списка во время прямого прохода по нему. Проще и безопаснее собрать новый результирующий список.
Итоговый чек-лист
- Определите поле или набор полей, по которым записи считаются дублями.
- Решите заранее, нужно сохранять первое или последнее вхождение.
- Для сохранения первого вхождения используйте
setуже встреченных ключей и отдельный список результата. - Убедитесь, что значение, помещаемое в
set, хешируемо. - Определите поведение для словарей без нужного ключа: ошибка, пропуск или отдельная группа.
- Для нескольких полей используйте кортеж вида
(item["a"], item["b"]). - Проверяйте результат через
assertна небольшом наборе, содержащем как уникальные записи, так и дубликаты.