Как правильно и хорошо реализовать hash ()?
Что такое правильный и хороший способ реализации __hash__()
?
Я говорю о функции, которая возвращает хэш-код, который затем используется для вставки объектов в хэш-таблицы aka словари.
Как __hash__()
возвращает целое число и используется для "связывания" объектов в хэш-таблицы я предполагаю, что значения возвращаемого целого числа должны быть равномерно распределены для общих данных (чтобы минимизировать столкновения).
Что это хорошая практика, чтобы получить такие значения? Столкновения в проблема?
В моем случае у меня есть небольшой класс, который действует как контейнерный класс, содержащий некоторые ints, некоторые поплавки и строку.
5 ответов:
простой, правильный способ реализации
__hash__()
использовать ключевой кортеж. Это будет не так быстро, как специализированный хэш, но если вам это нужно, то вы, вероятно, должны реализовать тип в C.вот пример использования ключа для хэша и равенства:
class A(object): def __key(self): return (self.attr_a, self.attr_b, self.attr_c) def __eq__(x, y): return x.__key() == y.__key() def __hash__(self): return hash(self.__key())
и документация
__hash__
имеет больше информации, которая может быть ценной в некоторых конкретных обстоятельствах.
Джон Милликин предложил решение, подобное этому:
class A(object): def __init__(self, a, b, c): self._a = a self._b = b self._c = c def __eq__(self, othr): return ((self._a, self._b, self._c) == (othr._a, othr._b, othr._c)) def __hash__(self): return hash((self._a, self._b, self._c))
проблема с этим решением состоит в том, что
hash(A(a, b, c)) == hash((a, b, c))
. Другими словами, хэш сталкивается с кортежем его ключевых членов. Может быть, это не имеет значения очень часто на практике?The документация Python на
__hash__
предлагает объединить хэши субкомпонентов, используя что-то вроде XOR, что дает нам следующее:class B(object): def __init__(self, a, b, c): self._a = a self._b = b self._c = c def __eq__(self, othr): return (isinstance(othr, type(self)) and (self._a, self._b, self._c) == (othr._a, othr._b, othr._c)) def __hash__(self): return (hash(self._a) ^ hash(self._b) ^ hash(self._c) ^ hash((self._a, self._b, self._c)))
бонус: более надежные
__eq__
бросил туда для хорошей меры.обновление: как указывает Blckknght, изменение порядка a, b и c может вызвать проблемы. Я добавил дополнительный
^ hash((self._a, self._b, self._c))
чтобы зафиксировать порядок хэширования значений. Это финал^ hash(...)
может быть удален, если объединяемые значения не могут быть переупорядочены (например, если они имеют разные типы и, следовательно, значение_a
никогда не будет назначена_b
или_c
и т. д.).
пол Ларсон из Microsoft Research изучил широкий спектр хэш-функций. Он сказал мне, что
for c in some_string: hash = 101 * hash + ord(c)
работал на удивление хорошо для широкого спектра строк. Я обнаружил, что подобные полиномиальные методы хорошо работают для вычисления хэша различных подполей.
Я могу попробовать ответить на вторую часть вашего вопроса.
коллизии, вероятно, будут возникать не из самого хэш-кода, а из сопоставления хэш-кода с индексом в коллекции. Так, например, ваша хэш-функция может возвращать случайные значения от 1 до 10000, но если ваша хэш-таблица имеет только 32 записи, вы получите коллизии при вставке.
кроме того, я думаю, что коллизии будут разрешены коллекцией внутренне, и их много методы разрешения конфликтов. Самый простой (и худший), учитывая запись для вставки в индекс i, добавьте 1 к i, пока не найдете пустое место и не вставите туда. Поиск тогда работает таким же образом. Это приводит к неэффективным извлечениям для некоторых записей, так как вы можете иметь запись, которая требует прохождения всей коллекции, чтобы найти!
другие методы разрешения конфликтов сокращают время поиска, перемещая записи в хэш-таблице, когда элемент вставляется для распространения вещей. Этот увеличивает время вставки, но предполагает, что Вы читаете больше, чем вставляете. Существуют также методы, которые пытаются ветвить различные конфликтующие записи, чтобы записи кластеризовались в одном конкретном месте.
кроме того, если вам нужно изменить размер коллекции, вам нужно будет перефразировать все или использовать метод динамического хэширования.
короче, в зависимости от того, что вы используете хэш-код для вас, возможно, придется реализовать свой собственный метод разрешения коллизий. Если вы не храните их в коллекция, вы, вероятно, можете уйти с хэш-функцией, которая просто генерирует хэш-коды в очень большом диапазоне. Если это так, вы можете убедитесь, что ваш контейнер больше, чем нужно (чем больше, тем лучше конечно) в зависимости от ваших проблем памяти.
вот некоторые ссылки, если вы заинтересованы больше:
объединенное хэширование в Википедии
Википедия также имеет резюме различных методов разрешения коллизий :
кроме того, "Организация И Обработка Файлов " по Tharp охватывает много методов разрешения конфликтов широко. IMO это отличная ссылка для алгоритмов хэширования.
зависит от размера возвращаемого хэш-значения. Это простая логика, что если вам нужно вернуть 32-битный int на основе хэша из четырех 32-битных int, вы получите коллизии.
Я бы предпочел битовые операции. Например, следующий псевдокод C:
int a; int b; int c; int d; int hash = (a & 0xF000F000) | (b & 0x0F000F00) | (c & 0x00F000F0 | (d & 0x000F000F);
такая система могла бы работать и для поплавков, если бы вы просто взяли их как их битовое значение, а не фактически представляли значение с плавающей запятой, может быть, лучше.
для строк, у меня мало/нет идея.