Как сделать класс данных 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.