web-dev-qa-db-fra.com

Comment puis-je rendre une python dataclass lavable?

Disons que j'ai une classe de données en python3. Je veux pouvoir hacher et ordonner ces objets.

Je veux seulement qu'ils soient commandés/hachés sur id.

Je vois dans les documents que je peux simplement implémenter __hash__ et tout ça, mais j'aimerais que datacalsses fasse le travail pour moi car ils sont destinés à gérer cela.

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'
26
Brian C.

De les docs :

Voici les règles régissant la création implicite d'une méthode __hash__():

[...]

Si eq et frozen sont tous les deux vrais, par défaut dataclass() générera pour vous une méthode __hash__(). Si eq est vrai et frozen est faux, __hash__() sera défini sur None, le marquant comme non-lavable (ce qu'il est, car il est modifiable) . Si eq est faux, __hash__() restera intacte, ce qui signifie que la méthode __hash__() de la superclasse sera utilisée (si la superclasse est un objet, cela signifie qu'elle retombera au hachage basé sur l'ID).

Depuis que vous définissez eq=True et laissé frozen par défaut (False), votre classe de données n'est pas partageable.

Vous avez 3 options:

  • Ensemble frozen=True (en plus de eq=True), ce qui rendra votre classe immuable et lavable.
  • Ensemble unsafe_hash=True, ce qui créera un __hash__ mais laissez votre classe modifiable, ce qui risque de poser des problèmes si une instance de votre classe est modifiée alors qu'elle est stockée dans un dict ou un ensemble:

    cat = Category('foo', 'bar')
    categories = {cat}
    cat.id = 'baz'
    
    print(cat in categories)  # False
    
  • Implémentez manuellement un __hash__ méthode.
24
Aran-Fey

TL; DR

Utilisez frozen=True Conjointement avec eq=True (Ce qui rendra les instances immuables).

Réponse longue

De la docs :

__hash__() est utilisé par la fonction intégrée hash(), et lorsque des objets sont ajoutés à des collections hachées telles que des dictionnaires et des ensembles. Avoir une __hash__() implique que les instances de la classe sont immuables. La mutabilité est une propriété compliquée qui dépend de l'intention du programmeur, de l'existence et du comportement de __eq__(), et des valeurs de l'égaliseur et des drapeaux figés dans le décorateur dataclass().

Par défaut, dataclass() n'ajoutera pas implicitement une méthode __hash__() sauf si cela est sûr. Il n'ajoutera ni ne modifiera pas non plus une méthode __hash__() existante explicitement définie. La définition de l'attribut de classe __hash__ = None A une signification spécifique pour Python, comme décrit dans la documentation __hash__().

Si __hash__() n'est pas explicitement défini, ou s'il est défini sur None, alors dataclass() peut ajouter une méthode implicite __hash__(). Bien que cela ne soit pas recommandé, vous pouvez forcer dataclass() pour créer une méthode __hash__() avec unsafe_hash=True. Cela peut être le cas si votre classe est logiquement immuable mais peut néanmoins être mutée. Il s'agit d'un cas d'utilisation spécialisé et doit être soigneusement étudié.

Voici les règles régissant la création implicite d'une méthode __hash__(). Notez que vous ne pouvez pas avoir à la fois une méthode __hash__() explicite dans votre classe de données et définir unsafe_hash=True; cela se traduira par un TypeError.

Si eq et figé sont tous deux vrais, par défaut dataclass() générera une méthode __hash__() pour vous. Si eq est vrai et figé est faux, __hash__() sera défini sur Aucun, le marquant comme non-lavable (ce qui est le cas, car il est mutable). Si eq est faux, __hash__() restera intacte, ce qui signifie que la méthode __hash__() de la superclasse sera utilisée (si la superclasse est objet, cela signifie qu'elle retombera sur le hachage basé sur l'id ).

5
DeepSpace

Je voudrais ajouter une note spéciale pour l'utilisation de unsafe_hash.

Vous pouvez exclure les champs de la comparaison par hachage en définissant compare = False ou hash = False. (le hachage hérite par défaut de compare).

Cela peut être utile si vous stockez des nœuds dans un graphique mais que vous souhaitez les marquer comme visités sans casser leur hachage (par exemple s'ils se trouvent dans un ensemble de nœuds non visités ..).

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

Cela m'a pris heures pour comprendre ... D'autres lectures utiles que j'ai trouvées sont le doc python sur les classes de données. Spécifiquement voir la documentation du champ et les documentations d'argument de la classe de données. https://docs.python.org/3/library/dataclasses.html

4
Leo Ufimtsev