41. Удаление дубликатов объектов Author во множестве
Условие зададчи:
Из нескольких источников поступают данные об одном и том же авторе научных публикаций. Данные из каждого источника преобразуются в объект класса Author, после чего все объекты добавляются во множество set для автоматического удаления дублей.
Нужно доработать класс Author так, чтобы при добавлении объектов в множество дубликаты автоматически схлопывались в один объект.
Дубликатами считаются записи, у которых совпадает хотя бы один идентификатор или одно из значений полей. Например, если у двух авторов совпадает rinc_id, такие записи должны считаться одним и тем же автором, даже если имя записано по-разному.
Ожидаемое поведение по примеру на скрине: после добавления нескольких пересекающихся записей во множестве должен остаться один объект с объединенными данными.
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class Author:
name: Optional[List[str]] = None
rinc_id: Optional[List[str]] = None
scopus_id: Optional[List[str]] = None
if __name__ == '__main__':
all_author = set()
all_author.add(Author(['Иван'], ['123321']))
all_author.add(Author(['Иванов'], ['123321']))
all_author.add(Author(['Иванов'], scopus_id=['123321']))
all_author.add(Author(['Иван'], scopus_id=['123321']))
print(all_author) # -> {Author(name=['Иванов', 'Иван'], rinc_id=['123321'], scopus_id=['123321'])}
Спойлеры к решению
Подсказки
- Обычный
setсам по себе не умеет объединять данные объектов. set.add()может только добавить объект или не добавить его, если считает дубликатом.- Если объект уже есть в
set, новый объект просто отбрасывается, но данные старого объекта автоматически не обновляются. - Для корректного решения нужен отдельный контейнер, который при добавлении ищет пересечение и вручную объединяет данные.
- Дубликатами считаем авторов, у которых пересекается хотя бы одно значение в
name,rinc_idилиscopus_id. - При нахождении дубликата нужно объединить списки без повторов.
Решение
from dataclasses import dataclass, field
from typing import Iterable
@dataclass
class Author:
name: list[str] = field(default_factory=list)
rinc_id: list[str] = field(default_factory=list)
scopus_id: list[str] = field(default_factory=list)
def has_intersection(self, other: "Author") -> bool:
return (
bool(set(self.name) & set(other.name))
or bool(set(self.rinc_id) & set(other.rinc_id))
or bool(set(self.scopus_id) & set(other.scopus_id))
)
def merge(self, other: "Author") -> None:
self.name = self._merge_lists(self.name, other.name)
self.rinc_id = self._merge_lists(self.rinc_id, other.rinc_id)
self.scopus_id = self._merge_lists(self.scopus_id, other.scopus_id)
@staticmethod
def _merge_lists(first: list[str], second: list[str]) -> list[str]:
result = []
for value in first + second:
if value not in result:
result.append(value)
return result
class AuthorSet:
def __init__(self):
self.authors: list[Author] = []
def add(self, author: Author) -> None:
for existing_author in self.authors:
if existing_author.has_intersection(author):
existing_author.merge(author)
return
self.authors.append(author)
def __iter__(self):
return iter(self.authors)
def __repr__(self):
return repr(set(map(repr, self.authors)))
Пример использования:
if __name__ == "__main__":
all_author = AuthorSet()
all_author.add(Author(["Иван"], ["123321"]))
all_author.add(Author(["Иванов"], ["123321"]))
all_author.add(Author(["Иванов"], scopus_id=["123321"]))
all_author.add(Author(["Иван"], scopus_id=["123321"]))
for author in all_author:
print(author)
Результат:
Author(name=['Иван', 'Иванов'], rinc_id=['123321'], scopus_id=['123321'])
Здесь обычный set заменён на специальный контейнер AuthorSet.
Причина: обычный set не подходит для такой логики. Он использует __hash__ и __eq__, чтобы понять, есть ли объект во множестве. Но если объект уже есть, set не вызывает никакого автоматического объединения данных. Новый объект просто не добавляется.
Поэтому правильная логика такая:
1. При добавлении нового автора пройтись по уже сохранённым авторам.
2. Проверить, есть ли пересечение по name, rinc_id или scopus_id.
3. Если пересечение есть — объединить данные.
4. Если пересечения нет — добавить автора как новую запись.
Если строго пытаться сделать это через обычный set, придётся мутировать объекты внутри __eq__, но это плохая практика. Такой код будет неочевидным, нестабильным и может нарушить правила работы set.
Более аккуратный вариант с сохранением вывода как у множества:
from dataclasses import dataclass, field
@dataclass
class Author:
name: list[str] = field(default_factory=list)
rinc_id: list[str] = field(default_factory=list)
scopus_id: list[str] = field(default_factory=list)
def has_intersection(self, other: "Author") -> bool:
return any([
set(self.name) & set(other.name),
set(self.rinc_id) & set(other.rinc_id),
set(self.scopus_id) & set(other.scopus_id),
])
def merge(self, other: "Author") -> None:
self.name = list(dict.fromkeys(self.name + other.name))
self.rinc_id = list(dict.fromkeys(self.rinc_id + other.rinc_id))
self.scopus_id = list(dict.fromkeys(self.scopus_id + other.scopus_id))
class AuthorSet:
def __init__(self):
self._items: list[Author] = []
def add(self, author: Author) -> None:
for item in self._items:
if item.has_intersection(author):
item.merge(author)
return
self._items.append(author)
def __repr__(self):
return "{" + ", ".join(repr(item) for item in self._items) + "}"
all_author = AuthorSet()
all_author.add(Author(["Иван"], ["123321"]))
all_author.add(Author(["Иванов"], ["123321"]))
all_author.add(Author(["Иванов"], scopus_id=["123321"]))
all_author.add(Author(["Иван"], scopus_id=["123321"]))
print(all_author)
Результат:
{Author(name=['Иван', 'Иванов'], rinc_id=['123321'], scopus_id=['123321'])}
Главный вывод: для автоматического удаления дублей без объединения достаточно set, но для удаления дублей с объединением данных нужен собственный контейнер или отдельная функция агрегации.