Может ли изменяться хеш-функция объекта во время его существования в Python?

Ответ

Нет, хеш объекта (значение, возвращаемое методом __hash__) должен оставаться постоянным на протяжении всего времени его жизни. Это критически важное правило для корректной работы хеш-таблиц, лежащих в основе словарей (dict), множеств (set) и frozenset. Если хеш изменится после помещения объекта в такую коллекцию, он станет недостижим.

Почему это важно: При вставке объекта Python вычисляет его хеш и использует его для определения «корзины» (bucket). При последующем поиске по тому же объекту вычисляется новый хеш. Если он отличается, поиск будет вестись в другой корзине, и объект не будет найден, хотя физически он там есть.

Пример проблемного класса:

class MutableHash:
    def __init__(self, name):
        self.name = name  # Используем атрибут для хеширования

    def __hash__(self):
        return hash(self.name)

    def __eq__(self, other):
        return isinstance(other, MutableHash) and self.name == other.name

obj = MutableHash('Alice')
my_set = {obj}
print(obj in my_set)  # True

obj.name = 'Bob'      # Меняем атрибут, от которого зависит хеш!
print(obj in my_set)  # False! Объект «потерялся»
print(hash(MutableHash('Alice'))) # Старый хеш
print(hash(obj))                  # Новый хеш. Они не совпадают.

Правила:

  • Неизменяемые встроенные типы (int, str, tuple) имеют постоянный хеш.
  • Изменяемые типы (list, dict, set) нехэшируемы (у них нет __hash__).
  • Пользовательские классы по умолчанию хэшируемы по id(), но если вы переопределяете __eq__, следует также переопределить __hash__, основывая его на неизменяемых атрибутах.

Ответ 18+ 🔞

Давай разжую эту тему, чтобы было понятно даже тому, кто только вчера узнал, что Python — это не только змея. Сиди, слушай, тут всё серьёзно, хотя и смешно до пиздеца.

Представь себе, что хеш объекта — это как его пожизненная татуировка в зоне. Ебушки-воробушки, её набивают один раз и навсегда. Если ты потом, будучи уже внутри словаря, решишь её перекрасить или замазать — всё, пиши пропало. Тебя система больше не опознает. Ты стал невидимкой в своей же собственной тюрьме, и найти тебя будет невозможно, хотя ты физически там сидишь. Удивление пиздец, да?

А почему, спрашивается, так строго? Всё просто, как три копейки. Когда объект ложится в dict или set, ему по этой татуировке (хешу) определяют конкретную камеру-корзину. И всё, приехали. Потом, когда охрана (интерпретатор) идёт проверять, на месте ли зек, она снова смотрит на татуху. А если ты её сменил? Ну, ёпта, они же ищут в старой камере по старому рисунку, а ты сидишь с новым — и тебя как будто и нет. Объект нахуй потерялся, хотя на самом деле он тут, под носом. Чистая магия, только хуёвая.

Смотри, какой прикол можно отмочить, если не знать правил:

class ГореИзобретатель:
    def __init__(self, имя):
        self.имя = имя  # Вот на этом поле хеш и завязан, блядь

    def __hash__(self):
        return hash(self.имя)

    def __eq__(self, other):
        return isinstance(other, ГореИзобретатель) and self.имя == other.имя

объект = ГореИзобретатель('Алиса')
моё_множество = {объект}
print(объект in моё_множество)  # True, пока всё ок, доверия ебать ноль

объект.имя = 'Боб'      # А вот тут начинается пиздец. Меняем атрибут, от которого хеш пляшет!
print(объект in моё_множество)  # False! Объект, блядь, испарился! Сам от себя охуел.
print(hash(ГореИзобретатель('Алиса'))) # Хеш был такой
print(hash(объект))                  # А стал вот такой. Совсем другой, ядрёна вошь.

Видишь, что происходит? Полный пиздец. Положили Алису, а достать уже не можем, потому что она теперь Боб. Коллекция в истерике, программа в ауте.

Так как же не облажаться? Запоминай правила, они проще, чем кажутся:

  • Неизменяемые типы (int, str, tuple) — они как монахи, дали обет и держат слово. Хеш у них постоянный, на них можно положиться.
  • Изменяемые типы (list, dict, set) — это вообще пизда рулю в этом контексте. Они нехэшируемые от слова «совсем». У них даже метода __hash__ нет, и слава богу, а то бы наделали делов.
  • Твои собственные классы — тут надо голову включать, э бошка думай. По умолчанию они хэшируются по своему id() (адресу в памяти), и это норм. Но! Если ты вдруг решил переопределить __eq__ (метод сравнения), то ты обязан переопределить и __hash__. И основывать его нужно строго на неизменяемых полях объекта. Как на скале высечь. Чтобы ни тебе, ни кому другому в голову не пришло это потом поменять.

Короче, суть в том, чтобы твой хеш вёл себя как паспорт, а не как настроение. Настроение меняется, а паспортные данные — нет. Иначе будет вам хиросима и нигерсраки в коде, ищи потом свои объекты по всем корзинам, волнение ебать.