¿Cómo puedo hacer que una clase de datos de Python sea hashable?

5 minutos de lectura

avatar de usuario
brian c

Digamos que tengo una clase de datos en python3. Quiero poder hacer hash y ordenar estos objetos.

Solo los quiero ordenados / hash en id.

Veo en los documentos que solo puedo implementar _picadillo_ y todo eso, pero me gustaría que datacalsses hicieran el trabajo por mí porque están destinados a manejar esto.

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'

  • Para encontrar ejemplos, consulte el Lo que puedes encender sección en esta publicación stackoverflow.com/a/52283085/4531270.

    – pilang

    30/11/2018 a las 20:55

De los documentos:

Estas son las reglas que rigen la creación implícita de un __hash__() método:

[…]

Si eq y frozen ambos son verdaderos, por defecto dataclass() generará un __hash__() método para ti. Si eq es cierto y frozen
Es falso, __hash__() se establecerá en None, marcándolo unhashable (que lo es, ya que es mutable). Si eq Es falso, __hash__()
se dejará intacto, lo que significa que el __hash__() Se usará el método de la superclase (si la superclase es un objeto, esto significa que recurrirá al hashing basado en id).

Desde que pusiste eq=True E izquierda frozen por defecto (False), su clase de datos no se puede modificar.

Tienes 3 opciones:

  • Establecer frozen=True (además de eq=True), lo que hará que su clase sea inmutable y hashable.
  • Establecer unsafe_hash=Trueque creará un __hash__ pero deje su clase mutable, lo que corre el riesgo de tener problemas si una instancia de su clase se modifica mientras está almacenada en un dictado o conjunto:

    cat = Category('foo', 'bar')
    categories = {cat}
    cat.id = 'baz'
    
    print(cat in categories)  # False
    
  • Implementar manualmente un __hash__ método.

  • Como se indica a continuación, para excluir algún campo del uso para picadillo generación en unsafe_hash, puede usar field(compare=False) o field(hash=False) (el hash hereda el valor de comparación si no está configurado).

    – Leo Ufimtsev

    29 de julio de 2019 a las 3:18

  • Tenga en cuenta que implementar manualmente __hash__() es trivial en el caso de un campo de tipo ID: def __hash__(self): return hash(self.id)

    – señora

    3 de enero de 2020 a las 21:33


Me gustaría agregar una nota especial para el uso de unsafe_hash.

Puede excluir campos de la comparación mediante hash configurando compare=False o hash=False. (hash por defecto hereda de comparar).

Esto podría ser útil si almacena nodos en un gráfico pero quiere marcarlos como visitados sin romper su hashing (por ejemplo, si están en un conjunto de nodos no visitados…).

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.")

esto me llevó horas para averiguarlo… Lecturas adicionales útiles que encontré es el documento de python sobre clases de datos. Consulte específicamente la documentación de campo y la documentación de argumento de clase de datos.
https://docs.python.org/3/library/dataclasses.html

  • Esto podría ser útil si almacena nodos en un gráfico pero quiere marcarlos como visitados sin romper su hashing (por ejemplo, si están en un conjunto de nodos no visitados…).: nunca me había sentido más dirigido por un caso de uso de ejemplo.

    – Álex Povel

    13 de diciembre de 2021 a las 21:43

avatar de usuario
Espacio profundo

TL;DR

Usar frozen=True en conjunto con eq=True (lo que hará que las instancias sean inmutables).

Respuesta larga

Desde el documentos:

__hash__() es utilizado por incorporado hash()y cuando se agregan objetos a colecciones con hash, como diccionarios y conjuntos. Teniendo un __hash__()
implica que las instancias de la clase son inmutables. La mutabilidad es una propiedad complicada que depende de la intención del programador, la existencia y el comportamiento de __eq__()y los valores de las banderas eq y frozen en el dataclass() decorador.

Por defecto, dataclass() no agregará implícitamente un __hash__() método a menos que sea seguro hacerlo. Tampoco agregará o cambiará un existente explícitamente definido __hash__() método. Establecer el atributo de clase
__hash__ = None tiene un significado específico para Python, como se describe en el __hash__() documentación.

Si __hash__() no está definido explícitamente, o si está configurado en Ninguno, entonces
dataclass() puede agregar un implícito __hash__() método. Aunque no se recomienda, puede forzar dataclass() para crear un __hash__() método con unsafe_hash=True. Este podría ser el caso si su clase es lógicamente inmutable pero, no obstante, se puede mutar. Este es un caso de uso especializado y debe ser considerado cuidadosamente.

Estas son las reglas que rigen la creación implícita de un __hash__() método. Tenga en cuenta que ambos no pueden tener un explícito __hash__() método en su clase de datos y establecer unsafe_hash=True; esto resultará en un TypeError.

Si eq y frozen son verdaderos, por defecto dataclass() generará un
__hash__() método para ti. Si eq es verdadera y frozen es falsa, __hash__() se establecerá en Ninguno, marcándolo como no modificable (que lo es, ya que es mutable). Si eq es falsa, __hash__() se dejará intacto, lo que significa que el __hash__() Se usará el método de la superclase (si la superclase es un objeto, esto significa que recurrirá al hashing basado en id).

¿Ha sido útil esta solución?