Как Python может иметь несколько ключей с одинаковым хэшем?

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

class C(object):
    def __hash__(self):
        return 42

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

c, d = C(), C()
x = {c: 'c', d: 'd'}
print x
# {<__main__.C object at 0x83e98cc>:'c', <__main__.C object at 0x83e98ec>:'d'}
# note that the dict has 2 elements

Я немного экспериментировал и обнаружил, что если я переопределю метод __eq__ так, чтобы все экземпляры класса сравнивались равными, то набор допускает только один экземпляр.

class D(C):
    def __eq__(self, other):
        return hash(self) == hash(other)

p, q = D(), D()
y = {p:'p', q:'q'}
print y
# {<__main__.D object at 0x8817acc>]: 'q'}
# note that the dict has only 1 element

Так что мне любопытно узнать, как может dict иметь несколько элементов с одинаковым хэшем. Спасибо!

Примечание. Отредактирован вопрос, чтобы привести пример dict (вместо set), потому что все обсуждения в ответах касаются dicts. Но то же самое относится к наборам; множества также могут иметь несколько элементов с одинаковым значением хеширования.

Ответ 1

Подробное описание того, как работает хеширование Python, см. мой ответ на Почему ранний возврат медленнее, чем другой?

В основном он использует хэш для выбора слота в таблице. Если в слоте есть значение и совпадение хэша, оно сравнивает элементы, чтобы убедиться, что они равны.

Если хеш не совпадает или элементы не равны, он пытается выполнить другой слот. Там есть формула, чтобы выбрать это (что я описываю в ответе), и он постепенно тянет неиспользованные части хэш-значения; но как только он все их использовал, он, в конечном счете, проложит все слоты в хеш-таблице. Это гарантирует, что в итоге мы найдем подходящий элемент или пустой слот. Когда поиск находит пустой слот, он вставляет значение или отбрасывается (в зависимости от того, добавляем ли мы или получаем значение).

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

Ответ 2

Здесь все о питонах Питона, которые я смог собрать (возможно, больше, чем кто-либо хотел бы знать, но ответ исчерпывающий). Выскажите Duncan, чтобы указать, что Python dicts использует слоты и приводит меня к этой кроличьей дыре.

  • Словари Python реализованы как хэш-таблицы.
  • Таблицы хэшей должны допускать хеш-коллизии, т.е. даже если два ключа имеют одинаковое значение хэширования, реализация таблицы должна иметь стратегию для вставки и извлечения ключей и значений однозначно.
  • Python dict использует открытую адресацию для разрешения хеш-коллизий (поясняется ниже) (см. dictobject.c: 296-297).
  • Хэш-таблица Python - это всего лишь ограниченный блок памяти (вроде массива, поэтому вы можете выполнять поиск O(1) по индексу).
  • Каждый слот в таблице может хранить одну и только одну запись. Это важно
  • Каждая запись в таблице фактически представляет собой комбинацию из трех значений - . Это реализовано как структура C (см. dictobject.h: 51-56)
  • Рисунок ниже представляет собой логическое представление хэш-таблицы python. На рисунке ниже, 0, 1,..., i,... слева находятся индексы слотов в хеш-таблице (они предназначены только для иллюстративных целей и не сохраняются вместе с таблица очевидно!).

    # Logical model of Python Hash table
    -+-----------------+
    0| <hash|key|value>|
    -+-----------------+
    1|      ...        |
    -+-----------------+
    .|      ...        |
    -+-----------------+
    i|      ...        |
    -+-----------------+
    .|      ...        |
    -+-----------------+
    n|      ...        |
    -+-----------------+
    
  • Когда инициализируется новый dict, он начинается с 8 слотов. (см. dictobject.h: 49)

  • При добавлении записей в таблицу, мы начинаем с некоторого слота i, который основан на хэше ключа. CPython использует начальный i = hash(key) & mask. Где mask = PyDictMINSIZE - 1, но это не очень важно). Просто отметьте, что начальный слот i, который проверяется, зависит от хэша ключа.
  • Если этот слот пуст, запись добавляется в слот (путем ввода, я имею в виду, <hash|key|value>). Но что, если этот слот занят!? Скорее всего, потому, что другая запись имеет тот же хеш (хеш-коллизия!)
  • Если слот занят, CPython (и даже PyPy) сравнивает хэш И ключ (по сравнению я имею в виду == сравнение не сравнение is) записи в слот против ключа текущей записи, которую нужно вставить (dictobject.c: 337,344-345). Если оба совпадают, то он считает, что запись уже существует, отбрасывается и переходит к следующей записи, которую нужно вставить. Если хэш или ключ не совпадают, он запускает зондирование.
  • Проверка просто означает, что он ищет слоты по слоту, чтобы найти пустой слот. Технически мы могли бы просто пойти один за другим, я + 1, я + 2,... и использовать первый доступный (это линейное зондирование). Но по причинам, прекрасно объясненным в комментариях (см. dictobject.c: 33-126), CPython использует случайное зондирование. При случайном зондировании следующий слот выбирается в псевдослучайном порядке. Запись добавляется в первый пустой слот. Для этого обсуждения фактический алгоритм, используемый для выбора следующего слота, не очень важен (см. dictobject.c: 33-126 для алгоритма для зондирования). Важно то, что слоты исследуются до тех пор, пока не будет найден первый пустой слот.
  • То же самое происходит и для поисков, просто начинается с начального слота я (где я зависит от хэша ключа). Если хэш и ключ не совпадают с входом в слот, он начинает зондирование, пока не найдет слот с совпадением. Если все слоты исчерпаны, он сообщает об ошибке.
  • BTW, диктофон будет изменен, если он будет заполнен на две трети. Это позволяет избежать замедления поиска. (см. dictobject.h: 64-65)

Иди сюда! Реализация Python проверок dict для хэш-равенства двух ключей и нормального равенства (==) ключей при вставке элементов. Итак, в общем случае, если есть два ключа: a и b и hash(a)==hash(b), но a!=b, то оба они могут гармонично существовать в диктофоне Python. Но если hash(a)==hash(b) и a==b, то они не могут оба находиться в одном и том же dict.

Поскольку мы должны исследовать после каждого столкновения с хэшем, один побочный эффект слишком большого количества хеш-коллизий заключается в том, что поисковые запросы и вставки будут очень медленными (как указывает Дункан в комментариях ).

Я предполагаю, что короткий ответ на мой вопрос: "Потому что, как это реализовано в исходном коде;)"

Пока это хорошо знать (для очков Geek?), я не уверен, как это можно использовать в реальной жизни. Потому что, если вы не пытаетесь явно что-то сломать, почему два объекта, которые не равны, имеют одинаковый хеш?

Ответ 3

Изменить. Ниже приведен один из возможных способов борьбы с хэш-коллизиями, однако не, как это делает Python. Ссылка на Python, приведенную ниже, также неверна. Лучший источник, данный @Duncan ниже, - это сама реализация: http://svn.python.org/projects/python/trunk/Objects/dictobject.c Извиняюсь за путаницу.


Он сохраняет список (или ведро) элементов в хеше, затем выполняет итерацию через этот список, пока не найдет фактический ключ в этом списке. На картине написано более тысячи слов:

Hash table

Здесь вы видите John Smith и Sandra Dee оба хэша <<22 > . Bucket 152 содержит оба из них. При поиске Sandra Dee он сначала находит список в bucket 152, затем перебирает этот список до тех пор, пока Sandra Dee не будет найден и не вернет 521-6955.

Ниже неверно это только для контекста: В Python wiki вы можете найти (псевдо?) как Python выполняет поиск.

На самом деле существует несколько возможных решений этой проблемы, посмотрите статью в википедии для хорошего обзора: http://en.wikipedia.org/wiki/Hash_table#Collision_resolution

Ответ 4

Таблицы хэшей, в общем, должны учитывать хэш-столкновения! Вам не повезет, и две вещи в конечном итоге будут хешировать к одному и тому же. Внизу есть набор объектов в списке элементов, который имеет тот же самый хеш-ключ. Обычно в этом списке есть только одно, но в этом случае он будет укладывать их в один и тот же. Единственный способ узнать, что они разные, - это оператор равенства.

Когда это произойдет, ваша производительность со временем ухудшится, поэтому вы хотите, чтобы ваша хэш-функция была как можно более "случайной".

Ответ 5

В потоке я не видел, что именно делает python с экземплярами пользовательских классов, когда мы помещаем его в словарь в качестве ключей. Давайте прочитаем некоторую документацию: она заявляет, что в качестве ключей можно использовать только хешируемые объекты. Hashable - это все неизменные встроенные классы и все пользовательские классы.

Пользовательские классы имеют __cmp __() и __hash __() по умолчанию; с ними все объекты сравнить неравные (за исключением самих себя) и x.__ hash __() возвращает результат, полученный из id (x).

Итак, если у вас есть постоянный __hash__ в вашем классе, но не предоставляется какой-либо метод __cmp__ или __eq__, тогда все ваши экземпляры не равны для словаря. С другой стороны, если вы предоставляете какой-либо метод __cmp__ или __eq__, но не предоставляете __hash__, ваши экземпляры по-прежнему неравны в терминах словаря.

class A(object):
    def __hash__(self):
        return 42


class B(object):
    def __eq__(self, other):
        return True


class C(A, B):
    pass


dict_a = {A(): 1, A(): 2, A(): 3}
dict_b = {B(): 1, B(): 2, B(): 3}
dict_c = {C(): 1, C(): 2, C(): 3}

print(dict_a)
print(dict_b)
print(dict_c)

Выход

{<__main__.A object at 0x7f9672f04850>: 1, <__main__.A object at 0x7f9672f04910>: 3, <__main__.A object at 0x7f9672f048d0>: 2}
{<__main__.B object at 0x7f9672f04990>: 2, <__main__.B object at 0x7f9672f04950>: 1, <__main__.B object at 0x7f9672f049d0>: 3}
{<__main__.C object at 0x7f9672f04a10>: 3}