Класс с изменением __hash__ по-прежнему работает со словарным доступом

То, что я сделал, очевидно, не то, что хотелось бы сделать, скорее, я просто тестировал реализацию __hash__ для данного класса.

Я хотел посмотреть, добавляет ли phony "hashable" класс в словарь, а затем изменение его хэш-значения приведет к тому, что он не сможет получить к нему доступ.

Мой класс выглядит следующим образом:

class PhonyHash:

    def __hash__(self):
        val = list("A string")
        return id(val)  # always different

Выполнение следующего в моей консоли IPython:

>>> p = PhonyHash()
>>> d = { p: "a value"}
>>> hash(p)  # changes hash

а затем попытка доступа к элементу с помощью d[p] работает:

>>> d[p]
"a value"

Я понимаю, это не то, что нужно сделать, мне просто интересно, почему это работает. Не использует dict объект hash() для хранения/извлечения объекта? Почему это работает?

изменить:, как указано в комментариях @VPfB sets, по каким-то причинам:

>>> p = PhonyHash()
>>> s = {p}
>>> p in s
False

Ответ 1

Это странная битва судьбы. Несколько бит оборудования CPython сорвали вас. Три проблемы в игре:

  • Начальный размер массива, который поддерживает dict, равен 8 [1]
  • Все объекты в CPython имеют адреса памяти по модулю 8 [2]
  • Класс dict имеет оптимизацию, которая проверяет ключи одного и того же объекта и останавливается там, если true (в противном случае он проверяет, равны ли они по методу __eq__) [3]

Это означает, что, несмотря на то, что ваш объект всегда создает разные значения хэша, первый слот массива поддержки, который будет рассмотрен, будет таким же. Если это не так, вы получите ключевую ошибку, потому что слот был пуст. Затем dict решает, что у него есть правильный ключ, потому что он имеет тот же самый объект, а не только равный объект.

class PhonyHash:
    _hash = 1
    def __hash__(self):
        return self._hash

p = PhonyHash()
d = {p: "val"}
print(p in d) # True
p._hash = 2
print(p in d) # False
p._hash = 9 # 9 % 8 == 1
print(p in d) # True

Ссылки на источники CPython

Ответ 2

У меня есть возможное объяснение:

Согласно этому источнику: http://www.laurentluce.com/posts/python-dictionary-implementation/, только несколько последних бит хэша используются, когда таблица, содержащая элементы dict, мала.

Номер id() обычно является машинным адресом и, вероятно, привязан к некоторой границе адреса памяти. Таким образом, последние несколько бит всегда равны нулю и не являются случайными вообще. В результате вы всегда нажимаете на элемент таблицы [0].

Попытка использовать другой источник случайного фальшивого хэша изменяет ситуацию и вызывается ожидаемый KeyError.


EDIT: Дюны ответили на вопрос таким же образом, и он был быстрее меня.