Avec PEP 557 , les classes de données sont introduites dans la bibliothèque standard python.
Ils utilisent le décorateur @dataclass
et ils sont supposés être des "noms 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 communes.
Que sont exactement les classes de données python et quand est-il préférable de les utiliser?
Les classes de données ne sont que des classes régulières orientées vers l'état de stockage. Elles contiennent plus que beaucoup de logique. Chaque fois que vous créez une classe composée principalement d'attributs, vous avez créé une classe de données.
Le module dataclasses
facilite la création de la création de classes de données. Il prend soin de beaucoup de plaque de la chaudière pour vous.
Ceci est particulièrement important lorsque votre classe de données doit être lavable; cela nécessite une méthode __hash__
ainsi qu'une méthode __eq__
. Si vous ajoutez une méthode personnalisée __repr__
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 dataclasses
, vous 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 gérer l’immuabilité.
Les classes namedtuple
sont aussi des classes de données, mais sont immuables par défaut (elles sont également des séquences). Les dataclasses
sont beaucoup plus souples à cet égard et peuvent facilement être structurés de manière à pouvoir remplir le même rôle qu'une classe namedtuple
.
Le PEP a été inspiré par le attrs
project , qui peut faire encore plus (y compris les créneaux horaires, les validateurs, les convertisseurs, les métadonnées, etc.).
Si vous voulez voir quelques exemples, j’ai récemment utilisé dataclasses
pour plusieurs de mes solutions Advent of Code , voir les solutions pour jour 7 , jour 8 , jour 11 et jour 2 .
Si vous voulez utiliser le module dataclasses
dans Python versions <3.7, vous pouvez installer le module avec port de retour (nécessite 3.6) ou utiliser le projet attrs
. mentionné ci-dessus.
La question a été abordée. Cependant, cette réponse ajoute quelques exemples pratiques facilitant 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?
namedtuple
et autres ."Tubes nommés mutables avec la valeur par défaut [s]"
Voici ce que cette dernière phrase signifie:
namedtuple
ou une classe normale.Par rapport aux classes courantes, vous économisez principalement sur la saisie du code standard.
Voici un aperçu des fonctionnalités de la classe de données (voir les exemples dans le tableau récapitulatif).
Voici les fonctionnalités que vous obtenez par défaut 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
_
Les valeurs par défaut suivantes sont automatiquement définies sur True
:
_@dataclasses.dataclass(init=True, repr=True, eq=True)
_
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 que functools.total_ordering
avec des tests d'égalité plus stricts.
Hashable, Mutable
_@dataclasses.dataclass(unsafe_hash=True) # override base `__hash__`
class Color:
...
_
Bien que l'objet soit potentiellement modifiable (éventuellement indésirable), un hachage est implémenté.
Hashable, Immutable
_@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.
Globalement, l'objet est obligatoire si _unsafe_hash=True
_ ou _frozen=True
_.
Voir aussi l'original table logique de hachage avec plus de détails.
Pour obtenir les fonctionnalités suivantes, des méthodes spéciales doivent être implémentées manuellement:
Déballable
_@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__
_ accélère également la création d'instances et l'accès aux attributs. De plus, les emplacements ne permettent pas les assignations par défaut; sinon, une ValueError
est levée.
Voir plus sur les créneaux horaires dans ce article de blog .
_+----------------------+----------------------+----------------------------------------------------+-----------------------------------------+
| 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__ |
| | | | |
| Unpackable+ | - | 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__
_ est non implémenté .
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
_
Conversions
Convertir une classe de données en un tuple ou un dict, récursivement :
_>>> dataclasses.astuple(Color(128, 0, 255))
(128, 0, 255)
>>> dataclasses.asdict(Color(128, 0, 255))
{r: 128, g: 0, b: 255}
_
Limites
Btw. Raymond Hettinger (développeur principal de Python) a eu une excellente conférence à la conférence PyCon 2018:
https://www.youtube.com/watch?v=T-TwcmT6Rcw&t=139
Les diapositives sont ici: https://Twitter.com/raymondh/status/995693882812915712
De la spécification PEP :
Un décorateur de classe est fourni pour inspecter une définition de classe pour les variables avec des annotations de type telles que définies dans PEP 526, "Syntaxe pour les annotations de variable". Dans ce document, ces variables s'appellent des champs. À l'aide de ces champs, le décorateur ajoute les définitions de méthode générées à la classe pour prendre en charge l'initialisation d'instance, une repr, les 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 elle n'a vraiment rien de spécial: le décorateur ajoute les méthodes générées à la classe et retourne la même classe qui lui a été donnée.
Le générateur @dataclass
ajoute à la classe les méthodes que vous définiriez autrement, comme __repr__
, __init__
, __lt__
et __gt__
.
Considérez cette classe simple Foo
from dataclasses import dataclass
@dataclass
class Foo:
def bar():
pass
Voici la comparaison intégrée dir()
. A gauche, Foo
sans le décorateur @dataclass et à droite, avec le décorateur @dataclass.
Voici un autre diff, après avoir utilisé le module inspect
à des fins de comparaison.