Какой правильный и хороший способ реализовать __hash __()?
Какой правильный и хороший способ реализовать __hash__()
?
Я говорю о функции, которая возвращает хэш-код, который затем используется для вставки объектов в hashtables aka словари.
Как __hash__()
возвращает целое число и используется для "бинирования" объектов в хэш-таблицы. Я предполагаю, что значения возвращаемого целого числа должны быть равномерно распределены для общих данных (чтобы минимизировать столкновения).
Какая хорошая практика для получения таких ценностей? Столкновение - проблема?
В моем случае у меня есть небольшой класс, который действует как контейнерный класс, содержащий некоторые int, некоторые float и строку.
Ответы
Ответ 1
Простой и правильный способ реализации __hash__()
- использовать кортеж ключа. Он не будет таким же быстрым, как специализированный хеш, но если вам это нужно, вам, вероятно, следует реализовать тип в C.
Вот пример использования ключа для хэша и равенства:
class A:
def __key(self):
return (self.attr_a, self.attr_b, self.attr_c)
def __hash__(self):
return hash(self.__key())
def __eq__(self, other):
if isinstance(other, A):
return self.__key() == other.__key()
return NotImplemented
Кроме того, в документации __hash__
содержится больше информации, которая может оказаться полезной в некоторых конкретных обстоятельствах.
Ответ 2
Джон Милликин предложил решение, подобное этому:
class A(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, self._b, self._c))
Проблема с этим решением в том, что hash(A(a, b, c)) == hash((a, b, c))
. Другими словами, хэш сталкивается с хэшем кортежа его ключевых членов. Может быть, это не имеет большого значения на практике?
Документация 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):
if isinstance(othr, type(self)):
return ((self._a, self._b, self._c) ==
(othr._a, othr._b, othr._c))
return NotImplemented
def __hash__(self):
return (hash(self._a) ^ hash(self._b) ^ hash(self._c) ^
hash((self._a, self._b, self._c)))
Обновление: как указывает Blckknght, изменение порядка a, b и c может вызвать проблемы. Я добавил дополнительный ^ hash((self._a, self._b, self._c))
, чтобы зафиксировать порядок хешируемых значений. Этот последний ^ hash(...)
может быть удален, если объединяемые значения нельзя переставить (например, если они имеют разные типы и, следовательно, значение _a
никогда не будет присвоено _b
или _c
и т.д.).
Ответ 3
Пол Ларсон из Microsoft Research изучил широкий спектр хеш-функций. Он сказал мне, что
for c in some_string:
hash = 101 * hash + ord(c)
работал на удивление хорошо для самых разных строк. Я обнаружил, что аналогичные методы полинома хорошо работают для вычисления хеша разрозненных подполей.
Ответ 4
Я могу попытаться ответить на вторую часть вашего вопроса.
Столкновения, вероятно, будут вызваны не самим хеш-кодом, а отображением хеш-кода на индекс в коллекции. Так, например, ваша хеш-функция может возвращать случайные значения от 1 до 10000, но если в вашей хеш-таблице только 32 записи, вы получите коллизии при вставке.
Кроме того, я бы подумал, что коллизии будут разрешаться коллекцией внутри, и есть много методов для разрешения коллизий. Простейшим (и худшим) является то, что с учетом записи, вставляемой по индексу i, добавляйте 1 к i, пока не найдете пустое место и не вставите туда. Поиск затем работает так же. Это приводит к неэффективным поискам для некоторых записей, поскольку у вас может быть запись, для поиска которой требуется пройти всю коллекцию!
Другие методы разрешения коллизий сокращают время поиска, перемещая записи в хеш-таблице, когда элемент вставляется, чтобы распределить объекты. Это увеличивает время вставки, но предполагает, что вы читаете больше, чем вставляете. Существуют также методы, которые пытаются разветвлять различные конфликтующие записи, чтобы записи кластеризовались в одном конкретном месте.
Кроме того, если вам нужно изменить размер коллекции, вам нужно будет перефразировать все или использовать метод динамического хеширования.
Короче говоря, в зависимости от того, что вы используете для хеш-кода, вам может потребоваться реализовать собственный метод разрешения коллизий. Если вы не храните их в коллекции, вы, вероятно, можете воспользоваться хеш-функцией, которая просто генерирует хеш-коды в очень большом диапазоне. Если это так, вы можете убедиться, что ваш контейнер больше, чем он должен быть (чем больше, тем лучше), в зависимости от ваших проблем с памятью.
Вот несколько ссылок, если вы заинтересованы больше:
объединенное хеширование в Википедии
В Википедии также есть сводка различных методов разрешения коллизий:
Кроме того, " Организация и обработка файлов " Tharp охватывает множество методов разрешения коллизий. ИМО это отличный справочник по алгоритмам хеширования.
Ответ 5
Зависит от размера возвращаемого значения хэша. Это простая логика: если вам нужно вернуть 32-битный int на основе хэша из четырех 32-битных ints, вы столкнетесь с конфликтами.
Я предпочитаю битовые операции. Например, следующий псевдо-код C:
int a;
int b;
int c;
int d;
int hash = (a & 0xF000F000) | (b & 0x0F000F00) | (c & 0x00F000F0 | (d & 0x000F000F);
Такая система могла бы работать и для float, если бы вы просто взяли их в качестве своего битового значения, а не представляли значение с плавающей запятой, может быть, лучше.
Для строк у меня мало/нет идеи.