Який правильний і хороший спосіб реалізувати __hash __ ()?


150

Який правильний і хороший спосіб втілити __hash__()?

Я говорю про функцію, яка повертає хеш-код, який потім використовується для вставки об'єктів у словники хеш-файлів.

Оскільки __hash__()повертає ціле число і використовується для "бінінгу" об'єктів у хештелі, я вважаю, що значення повернутого цілого числа повинні бути рівномірно розподілені для загальних даних (щоб мінімізувати зіткнення). Яка хороша практика для отримання таких цінностей? Чи зіткнення є проблемою? У моєму випадку у мене є невеликий клас, який виступає як клас контейнерів, що містить деякі ints, деякі floats та string.

Відповіді:


185

Простий, правильний спосіб реалізації __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__ містить більше інформації, яка може бути цінною в деяких конкретних обставинах.


1
Окрім незначних накладних витрат від розбивання __keyфункції, це приблизно так швидко, як і будь-який хеш. Звичайно, якщо атрибути, як відомо, є цілими числами, а їх не так вже й багато, я думаю, ви могли б потенційно запустити трохи швидше з домашнім прокатуваним хешем, але він, швидше за все, не буде так добре розподілений. hash((self.attr_a, self.attr_b, self.attr_c))буде напрочуд швидким (і правильним ), оскільки створення малих tuples спеціально оптимізовано, і це підштовхує роботу над отриманням та комбінуванням хешей до вбудованих C, що, як правило, швидше, ніж код рівня Python.
ShadowRanger

Скажімо, об’єкт класу A використовується як ключ для словника, і якщо атрибут класу A зміниться, його хеш-значення також зміниться. Хіба це не створило б проблеми?
Містер Матрікс

1
Як зазначено нижче у відповіді @ loved.by.Jesus, хеш-метод не повинен визначатися / змінюватись для об'єкта, що змінюється (визначається за замовчуванням і використовує id для рівності та порівняння).
Містер Матрікс

@ Мігель, я зіткнувся з точною проблемою , що трапляється - словник повертається, Noneколи ключ змінюється. Я вирішив це шляхом зберігання ідентифікатора об'єкта як ключа, а не просто об'єкта.
Jaswant P

@JaswantP Python за замовчуванням використовує ідентифікатор об'єкта в якості ключа для будь-якого об'єкта, що має доступ.
Містер Матрікс

22

Джон Міллікін запропонував подібне рішення:

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 тепер рекомендують використовувати кортеж, як у наведеному вище прикладі. Зауважте, що в документації зазначено

Єдиною необхідною властивістю є те, що об'єкти, які порівнюють рівні, мають однакове хеш-значення

Зауважте, що навпаки не вірно. Об'єкти, які не порівнюють рівних, можуть мати однакове хеш-значення. Таке зіткнення хешу не призведе до того, що один об'єкт замінить інший при використанні в якості ключа дікта або набору елементів до тих пір, поки об'єкти не порівнюються рівними .

Застаріле / погане рішення

Документація 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тощо).


5
Зазвичай ви не хочете робити прямі атрибути XOR разом, оскільки це призведе до зіткнення, якщо ви зміните порядок значень. Тобто, hash(A(1, 2, 3))буде дорівнює hash(A(3, 1, 2))(і вони обидва хеша рівні будь-якого іншого Aприкладу з перестановкою 1, 2і 3її цінності). Якщо ви хочете уникнути того, що ваш екземпляр має такий самий хеш, як кортеж їх аргументів, просто створіть значення дозорного (або змінна класу, або як глобальне), а потім включіть його в кортеж, який потрібно хешировать: return hash ((_ sentinel , self._a, self._b, self._c))
Blckknght

1
Ваше використання isinstanceможе бути проблематичним, оскільки об’єкт підкласу з type(self)тепер може дорівнювати об'єкту type(self). Таким чином , ви можете виявити , що додавання Carі Fordдо set()може привести тільки один об'єкт , вставлений в залежності від порядку введення. Крім того, ви можете зіткнутися з ситуацією, коли a == bце правда, але b == aпомилково.
MaratC

1
Якщо ви підкласифікуєте B, можливо, ви захочете змінити це наisinstance(othr, B)
millerdev

7
А думки: ключ кортеж може включати в себе тип класу, який запобіг би інші класи з тим же набором ключових атрибутів не з'являлися дорівнювати: hash((type(self), self._a, self._b, self._c)).
Бен Мошер

2
Окрім питання про використання Bзамість type(self), також часто вважається кращою практикою повертатися, NotImplementedколи наштовхується на несподіваний тип __eq__замість False. Це дозволяє іншим визначеним користувачем типам реалізовувати відомості, __eq__які знають про них, Bі, якщо вони хочуть, можуть порівнювати його з рівними.
Марк Амері

16

Пол Ларсон з Microsoft Research вивчав широкий спектр хеш-функцій. Він мені це сказав

for c in some_string:
    hash = 101 * hash  +  ord(c)

працював напрочуд добре для найрізноманітніших струн. Я виявив, що подібні методи поліномів добре допомагають обчислити хеш розрізнених підполів.


8
Мабуть, Java робить це так само, але використовуючи 31, а не 101
user229898

3
Яке обґрунтування використання цих чисел? Чи є причина вибрати 101, або 31?
bigblind

1
Ось пояснення для простих множників: stackoverflow.com/questions/3613102 / ... . 101, здається, працює особливо добре, спираючись на експерименти Пола Ларсона.
Джордж В. Рейлі

4
Python використовує (hash * 1000003) XOR ord(c)для рядків з 32-розрядним оберненим множенням. [Цитування ]
tylerl

4
Навіть якщо це правда, у цьому контексті це практично не застосовується, оскільки вбудовані типи рядків Python вже забезпечують __hash__метод; нам не потрібно катати свою. Питання полягає в тому, як реалізувати __hash__типовий визначений користувачем клас (з купою властивостей, що вказують на вбудовані типи або, можливо, на інші такі визначені користувачем класи), на які ця відповідь взагалі не звертається.
Марк Амері

3

Я можу спробувати відповісти на другу частину вашого питання.

Зіткнення, ймовірно, виникне не від самого хеш-коду, а від відображення хеш-коду до індексу в колекції. Так, наприклад, ваша хеш-функція може повертати випадкові значення від 1 до 10000, але якщо у вашій хеш-таблиці є лише 32 записи, ви отримаєте зіткнення при вставці.

Крім того, я думаю, що зіткнення будуть вирішені колекцією внутрішньо, і існує багато методів вирішення зіткнень. Найпростіший (і найгірший) спосіб - це дано запис, який потрібно вставити в індекс i, додати 1 до i, поки ви не знайдете порожнє місце і вкладете туди. Потім пошук працює аналогічно. Це призводить до неефективних пошуків для деяких записів, оскільки ви можете мати запис, який вимагає обходу всієї колекції, щоб знайти!

Інші способи вирішення зіткнення скорочують час пошуку, переміщуючи записи в хеш-таблиці, коли елемент вставляється для розповсюдження речей. Це збільшує час вставки, але передбачає, що ви читаєте більше, ніж вставляєте. Існують також методи, які намагаються розгалужувати різні записи, що зіштовхуються, так що записи кластеруються в одному конкретному місці.

Крім того, якщо вам потрібно змінити розмір колекції, вам потрібно буде повторно переробити все або використовувати метод динамічного хешування.

Якщо коротко, залежно від того, для чого ви використовуєте хеш-код, можливо, вам доведеться реалізувати власний метод вирішення зіткнення. Якщо ви не зберігаєте їх у колекції, ви, ймовірно, можете піти з хеш-функції, яка просто генерує хеш-коди у дуже великому діапазоні. Якщо так, ви можете переконатися, що ваш контейнер більший, ніж потрібно (чим більший, тим краще), залежно від вашої пам'яті.

Ось декілька посилань, якщо вас більше цікавить:

з’єднане хешування на вікіпедії

У Вікіпедії також є коротка інформація про різні методи вирішення колізій:

Також " Організація та обробка файлів " від Тарпа широко висвітлює багато методів вирішення зіткнень. IMO - це чудова посилання для алгоритмів хешування.


1

Дуже вдале пояснення того, коли і як реалізувати __hash__функцію на веб-сайті programiz :

Просто скріншот для надання огляду: (Отримано 2019-12-13)

Знімок екрана https://www.programiz.com/python-programming/methods/built-in/hash 2019-12-13

Що стосується особистої реалізації методу, згаданий вище сайт надає приклад, який відповідає відповіді millerdev .

class Person:
def __init__(self, age, name):
    self.age = age
    self.name = name

def __eq__(self, other):
    return self.age == other.age and self.name == other.name

def __hash__(self):
    print('The hash is:')
    return hash((self.age, self.name))

person = Person(23, 'Adam')
print(hash(person))

0

Залежить від величини хеш-значення, яке ви повернете. Проста логіка: якщо вам потрібно повернути 32-бітовий int на основі хеша з чотирьох 32-бітових ints, ви отримаєте зіткнення.

Я віддав би перевагу операціям з бітом. Мовляв, наступний псевдо-код C:

int a;
int b;
int c;
int d;
int hash = (a & 0xF000F000) | (b & 0x0F000F00) | (c & 0x00F000F0 | (d & 0x000F000F);

Така система може працювати і для поплавків, якби ви просто прийняли їх як їх бітове значення, а не насправді представляли значення з плаваючою комою, можливо, краще.

Щодо струн, у мене мало / ідеї немає.


Я знаю, що будуть зіткнення. Але я не маю поняття, як з цим поводитися. А далі мої значення атрибутів у поєднанні дуже рідко розподіляються, тому я шукав розумне рішення. І я якось очікував, що десь буде найкраща практика.
користувач229898
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.