Как сделать класс данных Python хэшируемым?

Скажи, у меня есть класс данных в python3. Я хочу иметь возможность хешировать и упорядочивать эти объекты.

Я только хочу, чтобы они заказывали / хэшировали по id.

Я вижу в документах, что я могу просто реализовать __hash__ и все такое, но я бы хотел, чтобы datacalsses выполнял эту работу за меня, потому что они предназначены для этого.

from dataclasses import dataclass, field

@dataclass(eq=True, order=True)
class Category:
    id: str = field(compare=True)
    name: str = field(default="set this in post_init", compare=False)

a = sorted(list(set([ Category(id='x'), Category(id='y')])))

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'Category'

4 ответа

Решение

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

Вот правила, регулирующие неявное создание __hash__() метод:

[...]

Если eq а также frozen оба являются истинными, по умолчанию dataclass() будет генерировать __hash__() метод для вас. Если eq это правда и frozen ложно, __hash__() будет установлен в None, пометив его как не подлежащий изменению (который является изменяемым). Если eq ложно, __hash__() останется нетронутым, что означает __hash__() будет использован метод суперкласса (если суперкласс является объектом, это означает, что он вернется к хешированию на основе идентификаторов).

Так как вы установили eq=True и влево frozen по умолчанию (False), ваш класс данных не подлежит изменению.

У вас есть 3 варианта:

  • Задавать frozen=True (в дополнение к eq=True), что сделает ваш класс неизменным и хэшируемым.
  • Задавать unsafe_hash=True, который создаст __hash__ метод, но оставьте ваш класс изменчивым, что может создать проблемы, если экземпляр вашего класса будет изменен во время хранения в dict или set:

    cat = Category('foo', 'bar')
    categories = {cat}
    cat.id = 'baz'
    
    print(cat in categories)  # False
    
  • Вручную реализовать __hash__ метод.

TL;DR

использование frozen=True в сочетании с eq=True (что сделает экземпляры неизменяемыми).

Длинный ответ

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

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

По умолчанию, dataclass() не будет неявно добавить __hash__() метод, если это не безопасно. Также он не будет добавлять или изменять существующие явно определенные __hash__() метод. Установка атрибута класса __hash__ = None имеет особое значение для Python, как описано в __hash__()документация.

Если __hash__() не определено явно, или если для него установлено значение Нет, то dataclass() может добавить неявное __hash__() метод. Хотя это не рекомендуется, вы можете заставить dataclass() создать __hash__() метод с unsafe_hash=True, Это может иметь место, если ваш класс логически неизменен, но, тем не менее, может быть видоизменен. Это специализированный вариант использования, и его следует тщательно рассмотреть.

Вот правила, регулирующие неявное создание __hash__() метод. Обратите внимание, что вы не можете иметь явное __hash__() метод в вашем классе данных и установить unsafe_hash=True; это приведет к TypeError,

Если eq и frozen оба имеют значение true, по умолчанию dataclass() будет генерировать __hash__() метод для вас. Если eq истинно, а заморожено ложно, __hash__() будет установлен на None, помечая его как недоступный (что и является изменяемым). Если eq ложно, __hash__() останется нетронутым, что означает __hash__() будет использован метод суперкласса (если суперкласс является объектом, это означает, что он вернется к хешированию на основе идентификаторов).

Я хотел бы добавить особое замечание по поводу использования unsafe_hash.

Вы можете исключить поля из сравнения по хешу, установив compare=False или hash=False. (хеш по умолчанию наследуется от compare).

Это может быть полезно, если вы храните узлы на графе, но хотите отметить их посещенными, не нарушая их хеширования (например, если они находятся в наборе непосещенных узлов...).

from dataclasses import dataclass, field
@dataclass(unsafe_hash=True)
class node:
    x:int
    visit_count: int = field(default=10, compare=False)  # hash inherits compare setting. So valid.
    # visit_count: int = field(default=False, hash=False)   # also valid. Arguably easier to read, but can break some compare code.
    # visit_count: int = False   # if mutated, hashing breaks. (3* printed)

s = set()
n = node(1)
s.add(n)
if n in s: print("1* n in s")
n.visit_count = 11
if n in s:
    print("2* n still in s")
else:
    print("3* n is lost to the void because hashing broke.")

Это заняло у меня часы, чтобы понять... Еще полезные материалы, которые я нашел, - это документ Python по классам данных. В частности, см. Полевую документацию и документацию аргументов класса данных. https://docs.python.org/3/library/dataclasses.html

Использовать:

      @dataclass(frozen=True, order=True)
class Category:

True — значение по умолчанию для eq.

Другие вопросы по тегам