Que sont les classes de données et en quoi sont-elles différentes des classes courantes?

141

Avec PEP 557, les classes de données sont introduites dans la bibliothèque standard python.

Ils utilisent le @dataclassdécorateur et ils sont supposés être des "tuples nommés mutables avec défaut" mais je ne suis pas vraiment sûr de comprendre ce que cela signifie réellement et en quoi ils sont différents des classes courantes.

Que sont exactement les classes de données Python et quand est-il préférable de les utiliser?

roiJulian
la source
8
Compte tenu du contenu étendu du PEP, que souhaiteriez-vous savoir d'autre? namedtuples sont immuables et ne peuvent pas avoir de valeurs par défaut pour les attributs, alors que les classes de données sont modifiables et peuvent les avoir.
jonrsharpe
31
@jonrsharpe me semble raisonnable qu'il devrait y avoir un thread stackoverflow sur le sujet. Stackoverflow est censé être une encyclopédie au format Q&R, non? La réponse n'est jamais "regardez simplement sur cet autre site Web". Il n'aurait pas dû y avoir de votes négatifs ici.
Luke Davis
12
Il existe cinq fils sur la façon d'ajouter un élément à une liste. Une question sur @dataclassne provoquera pas la désintégration du site.
eric
2
@jonrsharpe namedtuplesPEUT avoir des valeurs par défaut. Regardez ici: stackoverflow.com/questions/11351032/…
MJB

Réponses:

152

Les classes de données ne sont que des classes régulières orientées vers le stockage de l'état, plus que contenant beaucoup de logique. Chaque fois que vous créez une classe composée principalement d'attributs, vous créez une classe de données.

Le dataclassesmodule facilite la création de classes de données. Il s'occupe de beaucoup de plaques chauffantes pour vous.

Ceci est particulièrement important lorsque votre classe de données doit être hachable; cela nécessite une __hash__méthode aussi bien qu'une __eq__méthode. Si vous ajoutez une __repr__méthode personnalisée pour faciliter le débogage, cela peut devenir assez détaillé:

class InventoryItem:
    '''Class for keeping track of an item in inventory.'''
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def __init__(
            self, 
            name: str, 
            unit_price: float,
            quantity_on_hand: int = 0
        ) -> None:
        self.name = name
        self.unit_price = unit_price
        self.quantity_on_hand = quantity_on_hand

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

    def __repr__(self) -> str:
        return (
            'InventoryItem('
            f'name={self.name!r}, unit_price={self.unit_price!r}, '
            f'quantity_on_hand={self.quantity_on_hand!r})'

    def __hash__(self) -> int:
        return hash((self.name, self.unit_price, self.quantity_on_hand))

    def __eq__(self, other) -> bool:
        if not isinstance(other, InventoryItem):
            return NotImplemented
        return (
            (self.name, self.unit_price, self.quantity_on_hand) == 
            (other.name, other.unit_price, other.quantity_on_hand))

Avec dataclassesvous pouvez le réduire à:

from dataclasses import dataclass

@dataclass(unsafe_hash=True)
class InventoryItem:
    '''Class for keeping track of an item in inventory.'''
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

Le même décorateur de classe peut également générer des méthodes de comparaison ( __lt__, __gt__, etc.) et la poignée immuabilité.

namedtupleles classes sont également des classes de données, mais sont immuables par défaut (en plus d'être des séquences). dataclassessont beaucoup plus flexibles à cet égard et peuvent facilement être structurés de manière à pouvoir remplir le même rôle qu'une namedtupleclasse .

Le PEP s'est inspiré du attrsprojet , qui peut faire encore plus (y compris les slots, les validateurs, les convertisseurs, les métadonnées, etc.).

Si vous voulez voir quelques exemples, j'ai récemment utilisé dataclassespour plusieurs de mes solutions Advent of Code , voir les solutions pour les jours 7 , 8 , 11 et 20 .

Si vous souhaitez utiliser le dataclassesmodule dans les versions Python <3.7, vous pouvez installer le module rétroporté (nécessite 3.6) ou utiliser le attrsprojet mentionné ci-dessus.

Martijn Pieters
la source
2
Dans le premier exemple, cachez-vous intentionnellement des membres de classe avec des membres d'instance du même nom? Veuillez aider à comprendre cet idiome.
VladimirLenin
4
@VladimirLenin: il n'y a pas d'attributs de classe, il n'y a que des annotations de type. Voir PEP 526 , en particulier la section Annotations de variables de classe et d'instance .
Martijn Pieters
1
@Bananach: le @dataclassgénère à peu près la même __init__méthode, avec un quantity_on_handargument mot - clé avec la valeur par défaut. Lorsque vous créez une instance, il définira toujours l' quantity_on_handattribut d'instance. Donc, mon premier exemple non-dataclass utilise le même modèle pour faire écho à ce que le code généré par dataclass fera.
Martijn Pieters
1
@Bananach: donc dans le premier exemple, nous pourrions simplement omettre de définir un attribut d'instance et ne pas masquer l'attribut de classe, il est redondant de le définir de toute façon dans ce sens, mais les classes de données le définissent.
Martijn Pieters
1
@ user2853437 votre cas d'utilisation n'est pas vraiment pris en charge par les classes de données; vous feriez peut-être mieux d'utiliser le plus grand cousin des dataclasses , attrs . Ce projet prend en charge les convertisseurs par champ qui vous permettent de normaliser les valeurs de champ. Si vous voulez vous en tenir aux classes de données, alors oui, faites une normalisation dans la __post_init__méthode.
Martijn Pieters
62

Aperçu

La question a été abordée. Cependant, cette réponse ajoute quelques exemples pratiques pour aider à la compréhension de base des classes de données.

Que sont exactement les classes de données Python et quand est-il préférable de les utiliser?

  1. générateurs de code : générer un code standard; vous pouvez choisir d'implémenter des méthodes spéciales dans une classe régulière ou de les faire implémenter automatiquement par une classe de données.
  2. conteneurs de données : structures qui contiennent des données (par exemple, des tuples et des dicts), souvent avec des points d'accès aux attributs tels que les classes namedtupleet autres .

"tuples nommés mutables avec [s] par défaut"

Voici ce que signifie cette dernière phrase:

  • mutable : par défaut, les attributs de classe de données peuvent être réaffectés. Vous pouvez éventuellement les rendre immuables (voir les exemples ci-dessous).
  • namedtuple : vous avez un accès aux attributs pointillé comme une namedtupleclasse ou une classe normale.
  • par défaut : vous pouvez attribuer des valeurs par défaut aux attributs.

Par rapport aux classes courantes, vous économisez principalement sur la saisie de code standard.


Caractéristiques

Ceci est un aperçu des fonctionnalités de la classe de données (TL; DR? Voir le tableau récapitulatif dans la section suivante).

Ce que vous obtenez

Voici les fonctionnalités que vous obtenez par défaut à partir des classes de données.

Attributs + représentation + comparaison

import dataclasses


@dataclasses.dataclass
#@dataclasses.dataclass()                                       # alternative
class Color:
    r : int = 0
    g : int = 0
    b : int = 0

Ces valeurs par défaut sont fournies en définissant automatiquement les mots-clés suivants sur True:

@dataclasses.dataclass(init=True, repr=True, eq=True)

Ce que vous pouvez activer

Des fonctionnalités supplémentaires sont disponibles si les mots-clés appropriés sont définis sur True.

Ordre

@dataclasses.dataclass(order=True)
class Color:
    r : int = 0
    g : int = 0
    b : int = 0

Les méthodes de classement sont maintenant implémentées (opérateurs de surcharge:) < > <= >=, de la même manière functools.total_orderingqu'avec des tests d'égalité plus forts.

Hashable, Mutable

@dataclasses.dataclass(unsafe_hash=True)                        # override base `__hash__`
class Color:
    ...

Bien que l'objet soit potentiellement mutable (éventuellement indésirable), un hachage est implémenté.

Hashable, immuable

@dataclasses.dataclass(frozen=True)                             # `eq=True` (default) to be immutable 
class Color:
    ...

Un hachage est maintenant implémenté et la modification de l'objet ou l'attribution d'attributs est interdite.

Dans l'ensemble, l'objet peut être haché si l'un unsafe_hash=Trueou l' autre frozen=True.

Voir également la table logique de hachage d' origine avec plus de détails.

Ce que tu n'obtiens pas

Pour obtenir les fonctionnalités suivantes, des méthodes spéciales doivent être implémentées manuellement:

Déballage

@dataclasses.dataclass
class Color:
    r : int = 0
    g : int = 0
    b : int = 0

    def __iter__(self):
        yield from dataclasses.astuple(self)

Optimisation

@dataclasses.dataclass
class SlottedColor:
    __slots__ = ["r", "b", "g"]
    r : int
    g : int
    b : int

La taille de l'objet est maintenant réduite:

>>> imp sys
>>> sys.getsizeof(Color)
1056
>>> sys.getsizeof(SlottedColor)
888

Dans certaines circonstances, __slots__améliore également la vitesse de création des instances et d'accès aux attributs. De plus, les emplacements n'autorisent pas les attributions par défaut; sinon, a ValueErrorest levé.

En savoir plus sur les machines à sous dans cet article de blog .


Sommaire

+----------------------+----------------------+----------------------------------------------------+-----------------------------------------+
|       Feature        |       Keyword        |                      Example                       |           Implement in a Class          |
+----------------------+----------------------+----------------------------------------------------+-----------------------------------------+
| Attributes           |  init                |  Color().r -> 0                                    |  __init__                               |
| Representation       |  repr                |  Color() -> Color(r=0, g=0, b=0)                   |  __repr__                               |
| Comparision*         |  eq                  |  Color() == Color(0, 0, 0) -> True                 |  __eq__                                 |
|                      |                      |                                                    |                                         |
| Order                |  order               |  sorted([Color(0, 50, 0), Color()]) -> ...         |  __lt__, __le__, __gt__, __ge__         |
| Hashable             |  unsafe_hash/frozen  |  {Color(), {Color()}} -> {Color(r=0, g=0, b=0)}    |  __hash__                               |
| Immutable            |  frozen + eq         |  Color().r = 10 -> TypeError                       |  __setattr__, __delattr__               |
|                      |                      |                                                    |                                         |
| Unpacking+           |  -                   |  r, g, b = Color()                                 |   __iter__                              |
| Optimization+        |  -                   |  sys.getsizeof(SlottedColor) -> 888                |  __slots__                              |
+----------------------+----------------------+----------------------------------------------------+-----------------------------------------+

+ Ces méthodes ne sont pas générées automatiquement et nécessitent une implémentation manuelle dans une classe de données.

* __ne__ n'est pas nécessaire et n'est donc pas implémenté .


Caractéristiques supplémentaires

Post-initialisation

@dataclasses.dataclass
class RGBA:
    r : int = 0
    g : int = 0
    b : int = 0
    a : float = 1.0

    def __post_init__(self):
        self.a : int =  int(self.a * 255)


RGBA(127, 0, 255, 0.5)
# RGBA(r=127, g=0, b=255, a=127)

Héritage

@dataclasses.dataclass
class RGBA(Color):
    a : int = 0

Les conversions

Convertissez une classe de données en tuple ou en dict, de manière récursive :

>>> dataclasses.astuple(Color(128, 0, 255))
(128, 0, 255)
>>> dataclasses.asdict(Color(128, 0, 255))
{r: 128, g: 0, b: 255}

Limites


Références

  • De R. Hettinger le discours sur Dataclasses: Le générateur de code pour mettre fin à tous les générateurs de code
  • T. Hunner Le discours sur Easier classes: classes Python sans tous les Cruft
  • Python Documentation sur les détails de hachage
  • Guide du vrai Python sur le guide ultime pour les classes de données en Python 3.7
  • Article de blog de A. Shaw sur Une brève visite des classes de données Python 3.7
  • Dépôt github d' E. Smith sur les classes de données
pylang
la source
2

De la spécification PEP :

Un décorateur de classe est fourni qui inspecte une définition de classe pour les variables avec des annotations de type comme défini dans PEP 526, "Syntaxe pour les annotations de variables". Dans ce document, ces variables sont appelées champs. À l'aide de ces champs, le décorateur ajoute des définitions de méthode générées à la classe pour prendre en charge l'initialisation d'instance, une repr, des méthodes de comparaison et éventuellement d'autres méthodes, comme décrit dans la section Spécification. Une telle classe s'appelle une classe de données, mais il n'y a vraiment rien de spécial à propos de la classe: le décorateur ajoute des méthodes générées à la classe et renvoie la même classe qui lui a été donnée.

Le @dataclassgénérateur ajoute des méthodes à la classe que vous auriez autrement vous définissez comme __repr__, __init__, __lt__et __gt__.

Mahmoud Hanafy
la source
2

Considérez cette classe simple Foo

from dataclasses import dataclass
@dataclass
class Foo:    
    def bar():
        pass  

Voici la dir()comparaison intégrée. Sur le côté gauche se trouve le Foodécorateur sans @dataclass, et à droite avec le décorateur @dataclass.

entrez la description de l'image ici

Voici un autre diff, après avoir utilisé le inspectmodule pour comparaison.

entrez la description de l'image ici

prosti
la source