Автор: 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
, который тоже определен по умолчанию. Часто люди думают, что хеш объекта совпадает с его адресом в памяти, но это не всегда так:
- в версиях Python 2.6 и ниже да, было просто
hash(x) = id()
- с версии 2.6 и выше:
hash(x)==id(x)/16
То есть, рассчитывать на то, что 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).