Дублирование объектов в множестве

Автор: CoolPython

Пусть у нас есть сервер и его клиенты. На сервере мы хотим учитывать состояние клиентов и управлять ими. Для этого будем добавлять клиенты в множество, чтобы случайно не учесть один и тот же клиент на сервере дважды.

Создадим класс Client и добавим его в множество (сразу определила метод repr, чтобы были красивые принты):

class Client:
    def __init__(self, user_name):
        self.user_name = user_name

    def __repr__(self):
        return self.user_name

fish1 = Client(user_name="catfish")

clients = set()
clients.add(fish1)

print(clients)  # {catfish}

Круто, все работает из коробки! Попробуем теперь добавить второй клиент, чтобы увидеть, что дублирования нет:

fish2 = Client(user_name="catfish")
clients.add(fish2)

print(clients)  # {catfish, catfish}

Как же так? Мы добавили в множество два совершенно одинаковых экземпляра и ждали, что останется только один, но сохранились оба.

Интерпретатор при добавлении объекта в множество следует правилу:

если a == b, то обязательно hash(a) == hash(b)

То есть, сравнивает между собой как сами объекты, так и их хеши.

Для того, чтобы сравнивать объекты, множества и словари используют магический метод eq. Для пользовательских объектов он определен по умолчанию. И по умолчанию два разных объекта не равны, даже если они абсолютно идентичны:

print(fish1 == fish2)  # False

По умолчанию все объекты в Python также хешируемы. Когда мы пытаемся добавить объект в множество, мы используем магический метод этого объекта hash, который тоже определен по умолчанию. Часто люди думают, что хеш объекта совпадает с его адресом в памяти, но это не всегда так:

То есть, рассчитывать на то, что hash(x) = id(), нельзя. Но можно рассчитывать на то, что в Python-объектах по умолчанию есть hash, который зависит от id() и на то, что хеш объекта в течение его жизни не меняется.

Так как в нашем примере объекты разные и располагаются в разных ячейках памяти, то

print(hash(fish1), hash(fish2))  # 8786876890805 8786876904409

Так что делать, чтобы объекты, которые мы хотим считать одинаковыми, схлопывались во множестве? Перегрузить методы hash и eq, чтобы в явном виде дать интерпретатору инструкцию, как сравнивать объекты и как рассчитывать хеш.

Для этого исправим определение класса:

class Client:
    def __init__(self, user_name):
        self.user_name = user_name

    def __repr__(self):
        return self.user_name
        
    def __hash__(self):
        return hash(self.user_name)

    def __eq__(self, other):
        if self.user_name == other.user_name:
            return True
        else:
            return False

Теперь при добавлении объектов в множество все работает как ожидали:

clients = set()

fish1 = Client(user_name="catfish")
clients.add(fish1)
fish2 = Client(user_name="catfish")
clients.add(fish2)

print(clients)  # {catfish}

Помните, для того, чтобы объект класса, который вы определили сами, можно было добавить во множество или словарь, нужно:

  • Перегрузить методы hash и eq, причем оба, иначе не заработает.
  • Если объекты идентичны, то и их хеши должны быть равны.
  • В хэш-функцию должно попадать то, что однозначно идентифицирует объект.
  • Хеш объекта не должен меняться в течение его жизни (иначе будете ловить чудеса в run time).