Как Python dict может иметь несколько ключей с одинаковым хешем? - PullRequest
76 голосов
/ 26 января 2012

Я пытаюсь понять хэш-функцию 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 (вместо set), потому что все обсуждения в ответах посвящены dicts. Но то же самое относится и к сетам; наборы также могут иметь несколько элементов с одинаковым значением хеш-функции.

Ответы [ 5 ]

93 голосов
/ 26 января 2012

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

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

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

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

Полагаю, короткий ответ на мой вопрос таков: «Вот как это реализовано в исходном коде;)»

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

41 голосов
/ 26 января 2012

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

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

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

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

19 голосов
/ 26 января 2012

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


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

Hash table

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

Следующее неверно, это только здесьдля контекста: На вики Python вы можете найти (псевдо?) код, как Python выполняет поиск.

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

4 голосов
/ 26 января 2012

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

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

2 голосов
/ 23 августа 2014

В потоке я не видел, что именно делает 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}
...