dataclasses
— Classes de Données¶
Code source : Lib/dataclasses.py
Ce module fournit un décorateur et des fonctions pour générer automatiquement les méthodes spéciales comme __init__()
et __repr__()
dans les Classes de Données définies par l’utilisateur. Ces classes ont été décrites dans la PEP 557.
Les variables membres à utiliser dans ces méthodes générées sont définies en utilisant les annotations de type PEP 526. Par exemple :
from dataclasses import dataclass
@dataclass
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
ce code ajoute à la classe, entre autres choses, une méthode __init__()
qui ressemble à :
def __init__(self, name: str, unit_price: float, quantity_on_hand: int = 0):
self.name = name
self.unit_price = unit_price
self.quantity_on_hand = quantity_on_hand
Il est important de noter que cette méthode est ajoutée automatiquement dans la classe : elle n’est pas à écrire dans la définition de InventoryItem
ci-dessus.
Nouveau dans la version 3.7.
Décorateurs, classes et fonctions au niveau du module¶
-
@
dataclasses.
dataclass
(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)¶ Cette fonction est un décorateur qui est utilisé pour ajouter les méthodes spéciales générées aux classes, comme décrit ci-dessous.
Le décorateur
dataclass()
examine la classe pour trouver des champs. Un champ est défini comme une variable de classe qui possède une annotation de type. À deux exceptions près décrites plus bas,dataclass()
ne prend pas en considération le type donné dans l'annotation.L’ordre des paramètres des méthodes générées est celui d’apparition des champs dans la définition de la classe.
Le décorateur
dataclass()
ajoute diverses méthodes spéciales à la classe, décrites ci-après. Si l’une des méthodes ajoutées existe déjà dans la classe, le comportement dépend des paramètres. Le décorateur renvoie la classe sur laquelle il est appelé ; aucune nouvelle classe n'est créée.Si
dataclass()
est utilisé comme simple décorateur sans paramètres, il se comporte comme si on l’avait appelé avec les valeurs par défaut présentes en signature. Ainsi, les trois usages suivants dedataclass()
sont équivalents :@dataclass class C: ... @dataclass() class C: ... @dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False) class C: ...
Les paramètres de
dataclass()
sont :init
: Si vrai (par défaut), une méthode__init__()
est générée.Si la classe définit déjà une méthode
__init__()
, ce paramètre est ignoré.repr
: Si vrai (par défaut), une méthode__repr__()
sera générée. La chaîne de représentation comportera le nom de la classe et le nom ainsi que la représentation de chaque champ, suivant leur ordre de définition. Les champs marqués comme exclus (voirField
ci-dessous) de la représentation ne sont pas inclus. Par exemple :InventoryItem(name='widget', unit_price=3.0, quantity_on_hand=10)
.Si la classe définit déjà une méthode
__repr__()
, ce paramètre est ignoré.eq
: Si vrai (par défaut), une méthode__eq__()
est générée. Cette méthode permet de comparer les instances de la classe comme s’il s’agissait d’un n-uplet de ses champs, dans l’ordre. Les deux instances dans la comparaison doivent être de même type.Si la classe définit déjà une méthode
__eq__()
, ce paramètre est ignoré.order
: Si vrai (False
par défaut), les méthodes__lt__()
,__le__()
,__gt__()
, et__ge__()
sont générées. Elles permettent de comparer les instances de la classe en les considérant comme des n-uplets, dans l’ordre de définition des champs. Chaque instance dans la comparaison doit être de même type. Siorder
est vrai mais queeq
est faux, uneValueError
est levée.If the class already defines any of
__lt__()
,__le__()
,__gt__()
, or__ge__()
, thenTypeError
is raised.unsafe_hash
: SiFalse
(par défaut), une méthode__hash__()
est générée et son comportement dépend des valeurs deeq
etfrozen
.__hash__()
est utilisée par la fonction nativehash()
, ainsi que lorsqu’un objet est inséré dans une collection utilisant du hachage, tel qu’un dictionnaire ou un ensemble. Avoir une méthode__hash__()
implique que les instances de la classe sont immuables. La muabilité est une propriété complexe qui dépends des intentions du programmeur, de l’existence et du comportement de la méthode__eq__()
, et des valeurs des optionseq
etfrozen
dans l’appel au décorateurdataclass()
.Par défaut,
dataclass()
n’ajoute pas de méthode implicite__hash__()
, sauf s’il n’existe aucun risque sous-jacent. Il n’ajoute ou ne modifie pas non plus la méthode__hash__()
si elle a est définie explicitement. Définir l’attribut de classe__hash__ = None
a une signification particulière en Python, comme précisé dans la documentation de__hash__()
.Si
__hash__()
n’est pas défini explicitement, ou s’il a pour valeurNone
, alorsdataclass()
peut ajouter une méthode__hash__()
implicite. Bien que ce ne soit pas recommandé, vous pouvez forcerdataclass()
à créer une méthode__hash__()
en utilisantunsafe_hash=True
. Cela pourrait être nécessaire si votre classe est logiquement immuable mais qu’une mutation est tout de même possible. Il s'agit là d'un cas particulier qui doit être considéré avec la plus grande prudence.Ce sont les règles autour de la création implicite de la méthode
__hash__()
. Il faut noter que vous ne pouvez pas avoir à la fois une méthode__hash__()
explicite dans votre dataclass et définirunsafe_hash=True
; cela lèvera uneTypeError
.Si
eq
etfrozen
sont tous deux vrais,dataclass()
génère par défaut une méthode__hash__()
pour vous. Sieq
est vrai mais quefrozen
est faux,__hash__()
prend la valeurNone
, marquant la classe comme non-hachable (et c’est le cas, puisqu’elle est modifiable). Sieq
est faux, la méthode__hash__()
est laissée intacte, ce qui veut dire que la méthode__hash__()
de la classe parente sera utilisée (si la classe parente estobject
, le comportement est un hachage basé sur les id).frozen : si vrai (faux par défaut), assigner une valeur à un champ lève une exception. Cela simule le comportement des instances figées en lecture seule. Si la méthode
__setattr__()
ou__delattr__()
est définie sur la classe, alors uneTypeError
est levée. Voir la discussion ci-dessous.
Les
field
s peuvent éventuellement spécifier une valeur par défaut, en utilisant la syntaxe Python normale :@dataclass class C: a: int # 'a' has no default value b: int = 0 # assign a default value for 'b'
Dans cet exemple,
a
etb
sont tous deux inclus dans la signature de la méthode générée__init__()
, qui est définie comme suit :def __init__(self, a: int, b: int = 0):
Une
TypeError
est levée si un champ sans valeur par défaut est défini après un champ avec une valeur par défaut. C’est le cas que ce soit dans une seule classe ou si c’est le résultat d’un héritage de classes.
-
dataclasses.
field
(*, default=MISSING, default_factory=MISSING, repr=True, hash=None, init=True, compare=True, metadata=None)¶ Dans les cas les plus simples et courants, ce qui a été décrit jusqu'ici suffit. Cependant, les classes de données possèdent des fonctionnalités supplémentaires fondées sur des métadonnées propres à chaque champ. Pour remplir ces métadonnées, il suffit de mettre un appel à la fonction
field()
à la place de la valeur par défaut, comme dans cet exemple :@dataclass class C: mylist: list[int] = field(default_factory=list) c = C() c.mylist += [1, 2, 3]
As shown above, the
MISSING
value is a sentinel object used to detect if thedefault
anddefault_factory
parameters are provided. This sentinel is used becauseNone
is a valid value fordefault
. No code should directly use theMISSING
value.Les paramètres de
field()
sont :default : s'il est fourni, il devient la valeur par défaut du champ. L'appel à
field()
est mis à la place normale de la valeur par défaut, d'où la nécessité de ce paramètre.default_factory : s'il est fourni, ce doit être un objet appelable sans argument. Il est alors appelé à chaque fois qu'il faut une valeur par défaut pour le champ. Ceci permet, entre autres choses, de définir des champs dont les valeurs par défaut sont muables. Une erreur se produit si default et default_factory sont donnés tous les deux.
init : si vrai (par défaut), la méthode
__init__()
générée possède un paramètre correspondant à ce champ.repr : si vrai (par défaut), le champ est inclus dans les chaînes construites par la méthode
__repr__()
générée.compare : si vrai (par défaut), le champ est considéré dans les comparaisons d'égalité et d'inégalité dans les méthodes générées
__eq__()
,__gt__()
, etc.hash : ce paramètre est un booléen ou
None
. La valeurFalse
force la prise en compte du champ dans la méthode__hash__()
générée, alors queFalse
force son exclusion.None
revient à mettre la même valeur que compare, ce qui est en général correct : il faut inclure dans le hachage les champs employés pour les comparaisons. Il est déconseillé de mettre ce paramètre à autre chose queNone
.Cependant, une raison légitime de mettre hash à
False
alors que compare est àTrue
est la concourance de trois facteurs : le champ est coûteux à hacher ; il est nécessaire pour les comparaisons d'égalité ; et il y a déjà d'autres champs qui participent au hachage des instances. À ce moment, on peut alors se passer du champ dans le hachage tout en le faisant participer aux comparaisons.metadata : ce paramètre est un tableau associatif (mapping en anglais). La valeur par défaut de
None
est prise comme un dictionnaire vide. Le tableau associatif devient accessible sur l'objetField
, sous la forme d'unMappingProxyType()
afin qu'il soit en lecture seule.
Si la valeur par défaut d'un champ est donnée dans un appel à
field()
(et pas directement), l'attribut correspondant de la classe est remplacé par cette valeur. Si le paramètre default n'est pas passé, l'attribut est simplement supprimé. De cette manière, après le passage du décorateurdataclass()
, les attributs de la classe contiennent les valeurs par défaut des champs exactement comme si elles avaient été définies directement. Par exemple :@dataclass class C: x: int y: int = field(repr=False) z: int = field(repr=False, default=10) t: int = 20
Après l'exécution de ce code, l'attribut de classe
C.z
vaut10
et l'attributC.t
vaut20
, alors que les attributsC.x
etC.y
n'existent pas.
-
class
dataclasses.
Field
¶ Les objets
Field
contiennent des informations sur les champs. Ils sont créés en interne, et on y accède à l'aide de la méthode au niveau du modulefields()
(voir plus bas). Les utilisateurs ne doivent jamais instancier un objetField
eux-mêmes. Les attributs documentés sont les suivants :name : le nom du champ ;
type : le type associé au champ par l'annotation ;
default
,default_factory
,init
,repr
,hash
,compare
, andmetadata
have the identical meaning and values as they do in thefield()
declaration.
D'autres attributs peuvent exister, mais ils sont privés et ne sont pas censés être inspectés. Le code ne doit jamais reposer sur eux.
-
dataclasses.
fields
(class_or_instance)¶ Renvoie un n-uplet d'objets
Field
correspondant aux champs de l'argument, à l'exclusion des pseudo-champsClassVar
ouInitVar
. L'argument peut être soit une classe de données, soit une instance d'une telle classe ; si ce n'est pas le cas, une exceptionTypeError
est levée.
-
dataclasses.
asdict
(obj, *, dict_factory=dict)¶ Convertit la classe de données
obj
en un dictionnaire (en utilisant la fonctiondict_factory
). Les clés et valeurs proviennent directement des champs. Les dictionnaires, listes, n-uplets et instances de classes de données sont parcourus récursivement. Les autres objets sont copiés aveccopy.deepcopy()
.Exemple d'utilisation de
asdict()
sur des classes de données imbriquées :@dataclass class Point: x: int y: int @dataclass class C: mylist: list[Point] p = Point(10, 20) assert asdict(p) == {'x': 10, 'y': 20} c = C([Point(0, 0), Point(10, 4)]) assert asdict(c) == {'mylist': [{'x': 0, 'y': 0}, {'x': 10, 'y': 4}]}
Pour créer une copie superficielle, la solution de contournement suivante peut être utilisée :
dict((field.name, getattr(obj, field.name)) for field in fields(obj))
asdict()
lèveTypeError
siobj
n'est pas une instance d'une classe de données.
-
dataclasses.
astuple
(obj, *, tuple_factory=tuple)¶ Convertit l'instance d'une classe de données
obj
en un n-uplet (en utilisant la fonctiontuple_factory
). Chaque classe de données est convertie vers un n-uplet des valeurs de ses champs. Cette fonction agit récursivement sur les dictionnaires, listes et n-uplets. Les autres objets sont copiés aveccopy.deepcopy()
.Pour continuer l'exemple précédent :
assert astuple(p) == (10, 20) assert astuple(c) == ([(0, 0), (10, 4)],)
Pour créer une copie superficielle, la solution de contournement suivante peut être utilisée :
tuple(getattr(obj, field.name) for field in dataclasses.fields(obj))
astuple()
lèveTypeError
siobj
n'est pas une instance d'une classe de données.
-
dataclasses.
make_dataclass
(cls_name, fields, *, bases=(), namespace=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)¶ Creates a new dataclass with name
cls_name
, fields as defined infields
, base classes as given inbases
, and initialized with a namespace as given innamespace
.fields
is an iterable whose elements are each eithername
,(name, type)
, or(name, type, Field)
. If justname
is supplied,typing.Any
is used fortype
. The values ofinit
,repr
,eq
,order
,unsafe_hash
, andfrozen
have the same meaning as they do indataclass()
.Cette fonction est pratique mais pas absolument nécessaire, puisqu'il suffit de créer par un moyen quelconque une classe avec l'attribut
__annotation__
et de lui appliquer la fonctiondataclass()
, qui la convertit en une classe de données. Par exemple, ceci :C = make_dataclass('C', [('x', int), 'y', ('z', int, field(default=5))], namespace={'add_one': lambda self: self.x + 1})
est équivalent à :
@dataclass class C: x: int y: 'typing.Any' z: int = 5 def add_one(self): return self.x + 1
-
dataclasses.
replace
(obj, /, **changes)¶ Crée un nouvel objet du même type que
obj
en affectant aux champs les valeurs données parchanges
. Siobj
n'est pas une classe de données,TypeError
est levée. Si une clé danschanges
ne correspond à aucun champ de l'instance,TypeError
est levée.L'objet renvoyé est créé à l'aide de la méthode
__init__()
de la classe de données, ce qui garantit que__post_init__()
est appelée (si elle existe).Si un champ d'initialisation (voir plus bas) n'a pas de valeur par défaut, il faut l'inclure dans l'appel à
replace()
afin qu'il soit passé à__init__()
et__post_init__()
.Si une clé de changes correspond à un champ défini avec
init=False
,ValueError
est levée.Prenez garde aux champs définis avec
init=False
dans un appel àreplace()
. Ils ne sont pas copiés automatiquement de l'instance source. C'est le rôle de__post_init__()
de les initialiser — ou pas. Les champs avecinit=False
doivent rarement être utilisés et seulement à bon escient. Si vous en avez, il peut être sage de traiter la copie des instances par des constructeurs de classe alternatifs, ou bien une méthode personnaliséereplace()
(ou un nom similaire).
-
dataclasses.
is_dataclass
(obj)¶ Renvoie
True
si l'argument est soit une classe de données, soit une instance d'une telle classe. Sinon, renvoieFalse
.Pour vérifier qu'un objet obj est une instance d'une classe de données, et non pas lui-même une classe de données, ajoutez le test
not isinstance(obj, type)
:def is_dataclass_instance(obj): return is_dataclass(obj) and not isinstance(obj, type)
Post-initialisation¶
Après avoir initialisé l'objet, la méthode __init__()
générée appelle la méthode __post_init__()
, si elle est définie. __post_init__()
est habituellement appelée avec un simple self.__post_init__()
. Cependant, si la classe comporte des champs d'initialisation (voir plus bas), leurs valeurs sont aussi passées à __post_init__()
telles qu'elles ont été fournies à __init__()
. Si la classe est créée avec init=False
, __post_init__()
n'est jamais appelée automatiquement.
Cette méthode permet, entre autres, d'initialiser des champs qui dépendent d'autres champs. Par exemple :
@dataclass
class C:
a: float
b: float
c: float = field(init=False)
def __post_init__(self):
self.c = self.a + self.b
Les méthodes __init__()
des classes mères ne sont pas appelées automatiquement par la méthode __init__()
que génère dataclass()
. S'il faut appeler ces méthodes __init__()
, il est courant de le faire dans __post_init__()
:
@dataclass
class Rectangle:
height: float
width: float
@dataclass
class Square(Rectangle):
side: float
def __post_init__(self):
super().__init__(self.side, self.side)
Notez toutefois qu'il n'est généralement pas nécessaire d'appeler une méthode __init__()
si elle a été générée automatiquement dans une classe de données, car la classe fille initialise elle-même les champs apportés par toute classe mère qui est aussi une classe de données.
Voir la section plus bas à propos des variables d'initialisation pour les moyens de passer des paramètres à __post_init__()
. Voir également l'avertissement sur le traitement par replace()
des champs définis avec init=False
.
Variables de classe¶
One of two places where dataclass()
actually inspects the type
of a field is to determine if a field is a class variable as defined
in PEP 526. It does this by checking if the type of the field is
typing.ClassVar
. If a field is a ClassVar
, it is excluded
from consideration as a field and is ignored by the dataclass
mechanisms. Such ClassVar
pseudo-fields are not returned by the
module-level fields()
function.
Variables d'initialisation¶
The other place where dataclass()
inspects a type annotation is to
determine if a field is an init-only variable. It does this by seeing
if the type of a field is of type dataclasses.InitVar
. If a field
is an InitVar
, it is considered a pseudo-field called an init-only
field. As it is not a true field, it is not returned by the
module-level fields()
function. Init-only fields are added as
parameters to the generated __init__()
method, and are passed to
the optional __post_init__()
method. They are not otherwise used
by dataclasses.
On peut par exemple imaginer un champ initialisé à partir d'une base de données s'il n'a pas reçu de valeur explicite :
@dataclass
class C:
i: int
j: int = None
database: InitVar[DatabaseType] = None
def __post_init__(self, database):
if self.j is None and database is not None:
self.j = database.lookup('j')
c = C(10, database=my_database)
Ici, fields()
renvoie des objets Field
correspondant à i
et à j
, mais pas à database
.
Instances figées¶
Bien qu'il ne soit pas possible de créer des objets Python strictement immuables, on peut rendre les instances d'une classe de données quasi immuables en passant frozen=True
au décorateur dataclass()
, ce qui lui fait générer des méthodes __setattr__()
et __delattr__()
. Celles-ci lèvent systématiquement l'exception FrozenInstanceError
.
Les performances sont légèrement moins bonnes avec frozen=True
car __init__()
doit passer par object.__setattr__()
au lieu de simples affectations pour initialiser les champs.
Héritage¶
Au moment de la création d'une classe de données, le décorateur dataclass()
parcourt toutes les classes mères dans l'ordre inverse de résolution des méthodes (donc en commençant par object
). À chaque fois qu'une classe de données est rencontrée, ses champs sont insérés dans un tableau associatif ordonné. Pour finir, les champs de la classe elle-même sont rajoutés. Toutes les méthodes générées utilisent en interne ce même tableau associatif. Puisqu'il est ordonné, les champs des classes filles écrasent ceux des classes mères. Voici un exemple :
@dataclass
class Base:
x: Any = 15.0
y: int = 0
@dataclass
class C(Base):
z: int = 10
x: int = 15
La liste finale des champs contient, dans l'ordre, x
, y
, z
. Le type de x
est int
, comme déclaré dans C
.
La méthode __init__()
générée pour C
ressemble à :
def __init__(self, x: int = 15, y: int = 0, z: int = 10):
Fabriques de valeurs par défaut¶
Le paramètre facultatif default_factory de
field()
est une fonction qui est appelée sans argument pour fournir des valeurs par défaut. Par exemple, voici comment donner la valeur par défaut d'une liste vide :mylist: list = field(default_factory=list)Si un champ avec fabrique est exclu de
__init__()
(parinit=False
), alors la fabrique est appelée par__init__()
pour chaque nouvelle instance, puisque c'est le seul moyen d'obtenir une valeur à laquelle initialiser le champ.
Valeurs par défaut muables¶
En Python, les valeurs par défaut des attributs sont stockées dans des attributs de la classe. Observez cet exemple, sans classe de données :
class C: x = [] def add(self, element): self.x.append(element) o1 = C() o2 = C() o1.add(1) o2.add(2) assert o1.x == [1, 2] assert o1.x is o2.xComme attendu, les deux instances de
C
partagent le même objet pour l'attributx
.Avec les classes de données, si ce code était valide :
@dataclass class D: x: List = [] def add(self, element): self.x += elementil générerait un code équivalent à :
class D: x = [] def __init__(self, x=x): self.x = x def add(self, element): self.x += element assert D().x is D().xThis has the same issue as the original example using class
C
. That is, two instances of classD
that do not specify a value forx
when creating a class instance will share the same copy ofx
. Because dataclasses just use normal Python class creation they also share this behavior. There is no general way for Data Classes to detect this condition. Instead, dataclasses will raise aTypeError
if it detects a default parameter of typelist
,dict
, orset
. This is a partial solution, but it does protect against many common errors.Pour qu'un champ d'un type muable soit par défaut initialisé à un nouvel objet pour chaque instance, utilisez une fonction de fabrique :
@dataclass class D: x: list = field(default_factory=list) assert D().x is not D().x
Exceptions¶
-
exception
dataclasses.
FrozenInstanceError
¶ Sous-classe de
AttributeError
, levée lorsqu'une méthode__setattr__()
ou__delattr__()
définie implicitement est appelée dans une classe de données définie avecfrozen=True
.