Нередко требуется удалять дубликаты инстансов класса. Для этого обычно используется либо циклы со сравнением некоторых атрибутов, либо тип данных
set()
.
При добавлении элемента в
set
происходит сравнение этого объекта по хешу. Если хеш совпадает с хешем уже существующего объекта, то происходит сравнение объектов на равенство. Если объекты равны, то новый объект не добавляется.
class A:
def __init__(self, pk: int):
self.pk = pk
def __repr__(self):
return f"{self.__class__.__name__}(pk={self.pk})"
set([A(pk=1), A(pk=2), A(pk=2)])
>>> {A(pk=1), A(pk=2), A(pk=2)}
Далее для краткости метод `__repr__()` я буду пропускатьПо умолчанию в расчёте хеша, помимо прочего, используется адрес в памяти, который можно получить с помощью функции
id()
, поэтому все объекты считаются разными. Чтобы изменить способ сравнения объектов нам требуется переопределить метод
__eq__()
class A:
def __init__(self, pk: int):
self.pk = pk
def __eq__(self, other):
return self.pk == other.pk
set([A(pk=1), A(pk=2), A(pk=2)])
>>> TypeError: unhashable type: 'A'
Теперь в дело вступает логика, описаная в документации.
Если вы переопределили
__eq__()
то следует переопределить и
__hash__()
.
class A:
def __init__(self, pk: int):
self.pk = pk
def __eq__(self, other):
return self.pk == other.pk
def __hash__(self):
return hash(self.pk)
set([A(pk=1), A(pk=2), A(pk=2)])
>>> {A(pk=1), A(pk=2)}
Отлично, теперь всё работает.
Этот же принцип действует и при наследовании. Допустим, вы создали дочерний класс
class B(A):
pass
set([B(pk=1), B(pk=2), B(pk=2)])
>>> {B(pk=1), B(pk=2)}
Теперь следует учитывать вот такое поведение
hash(A(1)) == hash(B(1))
>>> True
set([A(1), B(1)])
>>> {A(pk=1)}
Инстансы А и В могут считаться идентичными, если они имеют одинаковые значения атрибутов и хеш, что может привести к неожиданным результатам при использовании множеств. Нужно учесть это в методах:
class A:
...
def __eq__(self, other):
return isinstance(other, self.__class__) and self.pk == other.pk
def __hash__(self):
return hash((self.pk, self.__class__))
...
Но если вдруг решите как-то изменить способ сравнения в классе В...
class B(A):
def __eq__(self, other):
return abs(self.pk) == abs(other.pk)
set([B(pk=1), B(pk=2), B(pk=2)])
>>> TypeError: unhashable type: 'B'
Снова получите ошибку. Та же логика - при переопределении метода
__eq__()
в новом классе метод
__hash__()
автоматически становится
None
и его тоже требуется переопределить.
#tricks