Что делает пользовательский класс нехешируемым?

В документах говорится, что класс можно хэшировать, если он определяет __hash__ метод. и __eq__ метод. Однако:

class X(list):
  # read-only interface of `tuple` and `list` should be the same, so reuse tuple.__hash__
  __hash__ = tuple.__hash__

x1 = X()
s = {x1} # TypeError: unhashable type: 'X'

Что делает X нехешируемым?

Обратите внимание, что у меня должны быть идентичные списки (с точки зрения обычного равенства), которые будут хешированы до одного и того же значения; в противном случае я нарушу это требование к хешу функции:

Единственным обязательным свойством является то, что объекты, которые сравниваются равными, имеют одинаковое значение хеш-функции.

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


person max    schedule 20.04.2012    source источник
comment
Да, интерфейсы только для чтения одинаковы, но почему вы ожидаете, что tuple.__hash__ будет использовать только внешние интерфейсы своего собственного класса? Особенно при написании на C. Использование внешних интерфейсов было бы намного медленнее. Вы не можете разумно ожидать, что метод класса A будет работать для класса B, если только класс B не является подклассом класса A. Вы даже пытались вызвать x1.__hash__(), чтобы посмотреть, сработало ли это?   -  person Lennart Regebro    schedule 21.04.2012
comment
@LennartRegebro Да, согласен... См. мой последний комментарий к stackoverflow.com/a/10254636/336527... У меня просто заморозили мозги.   -  person max    schedule 21.04.2012


Ответы (5)


Просто присвоить методу __hash__ метод класса tuple недостаточно. На самом деле вы не сказали, как хэшировать по-другому. кортежи можно хэшировать, потому что они неизменяемы. Если вы действительно хотите, чтобы ваш конкретный пример работал, это может быть так:

class X2(list):
    def __hash__(self):
        return hash(tuple(self))

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

def __hash__(self):
    return hash("foobar"*len(self))
person jdi    schedule 20.04.2012
comment
Но разве tuple.__hash__ не является функцией, которая принимает кортеж и возвращает число? Как эта функция замечает, что мой объект на самом деле является list, а не tuple - API чтения для двух типов идентичен. - person max; 21.04.2012
comment
@max: tuple.__hash__ - это связанный метод класса кортежа. Вы не меняете то, что его реализация делает внутри этого метода для хэширования. Определите свое собственное. - person jdi; 21.04.2012
comment
hash((1,2,3)) совпадает с (1,2,3).__hash__; это то же самое, что и tuple.__hash__((1,2,3)), верно? Итак, tuple.__hash__ опирается на непубличный API класса tuple, и поэтому он выходит из строя со сбивающим с толку сообщением об ошибке при передаче экземпляра другого класса, который соответствует общедоступному API tuple? Полагаю, это объясняет... но немного неожиданно. - person max; 21.04.2012
comment
@max: В конечном итоге процедура хеширования определена в классах кортежей __hash__, и, не глядя на источник, я могу только предположить, что она специально предназначена для внутренних частей экземпляра кортежа. Я никоим образом не удивлен, что простая передача ссылки на метод вашему классу списка не сработала должным образом. - person jdi; 21.04.2012
comment
Методы @max обычно зависят от внутреннего устройства класса. Вы действительно ожидаете, что сможете взять реализацию метода класса A и применить ее к объекту класса B только из-за некоторого сходства в общедоступном API обоих классов? Тот факт, что tuple и list являются встроенными классами, реализованными в C, делает это еще менее вероятным для работы; на уровне Python, если объекты B имеют все атрибуты, необходимые для метода, который вы получили от A, тогда это может работать, но на уровне C мы говорим о структурах, массивах и указателях. - person Ben; 21.04.2012
comment
@ Бен, я согласен .. я не знаю, о чем я думал - person max; 21.04.2012

Из документов Python3:

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

Ссылка: object.__hash__(self)

Образец кода:

class Hashable:
    pass

class Unhashable:
    def __eq__(self, other):
        return (self == other)

class HashableAgain:
    def __eq__(self, other):
        return (self == other)

    def __hash__(self):
        return id(self)

def main():
    # OK
    print(hash(Hashable()))
    # Throws: TypeError("unhashable type: 'X'",)
    print(hash(Unhashable()))  
    # OK
    print(hash(HashableAgain()))
person kevinarpe    schedule 03.04.2015
comment
Должен ли __hash__ быть уникальным? Предположим, вы хотите, чтобы экземпляры HashableAgain сравнивались на основе критериев, определенных вами в __eq__. Можете ли вы просто вернуть постоянное целое число в __hash__? (Я действительно не понимаю, как хеш) используется для определения принадлежности объекта к набору. - person Minh Tran; 21.02.2018
comment
@MinhTran: В общем, хэш не уникальный, а относительно уникальный. Он используется для группирования значений на карте. Если вы используете постоянное значение для хэша, все значения будут отображаться в одном сегменте, поэтому производительность будет ужасной... но она все равно должна работать! - person kevinarpe; 22.02.2018

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

class X(object):
    def __init__(self, *args):
        self.tpl = args
    def __hash__(self):
        return hash(self.tpl)
    def __eq__(self, other):
        return self.tpl == other
    def __repr__(self):
        return repr(self.tpl)

x1 = X()
s = {x1}

который дает:

>>> s
set([()])
>>> x1
()
person ch3ka    schedule 20.04.2012
comment
Вы правы, во многих случаях это самое чистое и простое решение; +1 - person senderle; 21.04.2012

Если вы не изменяете экземпляры X после создания, почему вы не создаете подкласс кортежа?

Но я укажу, что на самом деле это не вызывает ошибки, по крайней мере, в Python 2.6.

>>> class X(list):
...     __hash__ = tuple.__hash__
...     __eq__ = tuple.__eq__
... 
>>> x = X()
>>> s = set((x,))
>>> s
set([[]])

Я не решаюсь сказать «работает», потому что это не делает то, что вы думаете.

>>> a = X()
>>> b = X((5,))
>>> hash(a)
4299954584
>>> hash(b)
4299954672
>>> id(a)
4299954584
>>> id(b)
4299954672

Он просто использует идентификатор объекта в качестве хэша. Когда вы на самом деле вызываете __hash__, вы все равно получаете сообщение об ошибке; аналогично для __eq__.

>>> a.__hash__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: descriptor '__hash__' for 'tuple' objects doesn't apply to 'X' object
>>> X().__eq__(X())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: descriptor '__eq__' for 'tuple' objects doesn't apply to 'X' object

Я понимаю, что внутренние компоненты Python по какой-то причине обнаруживают, что X имеет методы __hash__ и __eq__, но не вызывают их.

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

def __hash__(self):
    return hash(tuple(self))
person senderle    schedule 20.04.2012
comment
Мне очень жаль, этот вопрос вырван из контекста другого. Я просто был смущен этим конкретным поведением. Причина, по которой я список подклассов, немного сложна (см. обсуждение в комментариях к этот вопрос) . - person max; 21.04.2012
comment
Код не работает для меня в ActiveState Python 3.2. Возможно, поведение изменилось в последнее время? - person max; 21.04.2012
comment
Я использую Python 2.6. В любом случае вам не нужно такое поведение, потому что использование ids в качестве ключей не очень хорошая идея. Лучше просто преобразовать в кортеж и хешировать его. А на самом деле -- извините; это был просто довольно сбивающий с толку подход к проблеме для меня. - person senderle; 21.04.2012
comment
В Python 3 хэш кортежа действительно создает хэш объектов кортежа, а не только идентификатора кортежа, если я правильно понимаю код. - person Lennart Regebro; 21.04.2012
comment
@LennartRegebro, я думаю, что это должно быть верно и для Python 2; или, по крайней мере, я могу создать два кортежа с разными идентификаторами, которые оцениваются как равные и имеют один и тот же хэш. Описываемое здесь поведение применимо только к X объектам, определенным выше. - person senderle; 21.04.2012

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

@dataclass(frozen=True)
class YourClass:
    pass

как украшение вместо

@dataclass
class YourClass:
    pass
person Alex Joseph    schedule 04.03.2021