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

Документы говорят, что класс является хэшируемым, пока он определяет __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 unhashable?

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

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

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

5 ответов

Решение

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

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

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

def __hash__(self):
    return hash("foobar"*len(self))

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

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

Ссылка: объект.__ хеш __ (самостоятельно)

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

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()))

Что вы могли и должны сделать, основываясь на другом вопросе: не делайте ничего на подклассы, просто инкапсулируйте кортеж. Это прекрасно в 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
()

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

      @dataclass(frozen=True)
class YourClass:
    pass

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

      @dataclass
class YourClass:
    pass

Если вы не измените экземпляры 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))
Другие вопросы по тегам