"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 jamais écrite dans la
définition de "InventoryItem".

Nouveau dans la version 3.7.


Classe de données
=================

@dataclasses.dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False)

   Cette fonction est un *décorateur* qui ajoute aux classes des
   *méthodes spéciales* générées automatiquement. Voici une
   description plus détaillée.

   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é directement, il se comporte comme si
   on l’avait appelé sans argument (c.-à-d. en laissant les valeurs
   par défaut de sa signature). Ainsi, les trois usages suivants de
   "dataclass()" sont équivalents :

      @dataclass
      class C:
          ...

      @dataclass()
      class C:
          ...

      @dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False,
                 match_args=True, kw_only=False, slots=False)
      class C:
         ...

   Les paramètres de "dataclass()" sont les suivants :

   * *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__()" est
     générée.  La chaîne de représentation comporte 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 de la représentation (voir "Field" ci-dessous) sont
     ignorés.  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
     si elles étaient des *n*-uplet de leurs champs, pris 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.  Toutes les instances dans la comparaison doivent être de
     même type.  Si "order" est vrai mais que "eq" est faux, une
     "ValueError" est levée.

     Si la classe définit déjà l’une des méthodes "__lt__()",
     "__le__()", "__gt__()", ou "__ge__()", alors une "TypeError" est
     levée.

   * *unsafe_hash* : si "False" (par défaut), une méthode "__hash__()"
     est générée et son comportement dépend des valeurs de *eq* et
     *frozen*.

     "__hash__()" est utilisée par la fonction native "hash()", 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épend des
     intentions du programmeur, de l’existence et du comportement de
     la méthode "__eq__()", et des valeurs des options *eq* et
     *frozen* dans l’appel au décorateur "dataclass()".

     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 été définie explicitement.  Définir l’attribut de classe
     "__hash__ = None" a une signification particulière, comme précisé
     dans la documentation de "__hash__()".

     Si "__hash__()" n’est pas défini explicitement, ou s’il a pour
     valeur "None", alors "dataclass()" *peut* ajouter une méthode
     "__hash__()" implicite. Bien que ce ne soit pas recommandé, vous
     pouvez forcer "dataclass()" à créer une méthode "__hash__()" en
     utilisant "unsafe_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.

     Voici 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 classe de
     données et définir "unsafe_hash=True" ; cela lèvera une
     "TypeError".

     Si *eq* et *frozen* sont tous deux vrais, "dataclass()" génère
     par défaut une méthode "__hash__()" pour vous.  Si *eq* est vrai
     mais que *frozen* est faux, "__hash__()" prend la valeur "None",
     marquant la classe comme non-hachable (et c’est le cas, puisque
     les instances sont mutables).  Si "eq" 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 est "object", 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 une
     "TypeError" est levée.  Voir la discussion ci-dessous.

   * *match_args* : si vrai ("True" est la valeur par défaut), le
     *n*-uplet "__match_args__" est créé automatiquement depuis la
     liste des paramètres de la méthode "__init__()" générée (même si
     "__init__()" n'est pas générée, voir ci-dessus). Si faux ou si
     "__match_args__" est déjà défini dans la classe alors
     "__match_args__" n'est pas créé.

      Nouveau dans la version 3.10.

   * *kw_only* : si vrai ("False" est la valeur par défaut) alors tous
     les champs sont marqués comme exclusivement nommés. Si un champ
     est marqué comme exclusivement nommé, le seul impact est que le
     champ de la méthode "__init__()" générée devra être explicitement
     nommé lors de l'appel de la méthode "__init__()". Il n'y a aucun
     autre effet dans les autres aspects des classes de données. Voir
     l'entrée *paramètre* du glossaire pour plus d'informations. Voir
     également la section "KW_ONLY".

      Nouveau dans la version 3.10.

   * *slot* : si vrai ("False" est la valeur par défaut), l'attribut
     "__slots__" est généré et une nouvelle classe est renvoyée à la
     place de celle d'origine. Si "__slots__" est déjà défini dans la
     classe alors une "TypeError" est levée.

      Nouveau dans la version 3.10.

   Les champs peuvent éventuellement préciser 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" et "b" 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, init=True, repr=True, hash=None, compare=True, metadata=None, kw_only=MISSING)

   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]

   Comme le montre la signature, la constante "MISSING" est une valeur
   sentinelle pour déterminer si des paramètres ont été fournis par
   l'utilisateur. "None" ne conviendrait pas puisque c'est une valeur
   avec un sens qui peut être différent pour certains paramètres. La
   sentinelle "MISSING" est interne au module et ne doit pas être
   utilisée dans vos programmes.

   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.

   * *hash* : ce paramètre est un booléen ou "None". La valeur "False"
     force la prise en compte du champ dans la méthode "__hash__()"
     générée, alors que "False" 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 que "None".

     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.

   * *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.

   * *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'objet "Field", sous la forme d'un "MappingProxyType()" afin
     qu'il soit en lecture seule.

   * "kw_only" : Si vrai ce champ est marqué comme exclusivement
     nommé. Cela est utilisé lors du traitement des paramètres de la
     méthode "__init__()" générée.

      Nouveau dans la version 3.10.

   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écorateur "dataclass()", 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" vaut "10"
   et l'attribut "C.t" vaut "20", alors que les attributs "C.x" et
   "C.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 module "fields()" (voir plus bas). Les utilisateurs ne
   doivent jamais instancier un objet "Field" 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*, *metadata* et *kw_only* qui correspondent aux
        paramètres de "field()" et en prennent les valeurs.

   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-champs "ClassVar" ou
   "InitVar". 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
   exception "TypeError" est levée.

dataclasses.asdict(obj, *, dict_factory=dict)

   Convertit la classe de données "obj" en un dictionnaire (en
   utilisant la fonction "dict_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 avec
   "copy.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ève "TypeError" si "obj" 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 fonction "tuple_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 avec "copy.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ève "TypeError" si "obj" 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, match_args=True, kw_only=False, slots=False)

   Crée une nouvelle classe de données avec le nom *cls_name*. Les
   champs proviennent de l'objet itérable *fields*. Les classes mères
   sont lues dans *bases*. L'espace de nommage de la classe est
   initialisé par *namespace*. La forme la plus générale d'un élément
   de *fields* est un triplet "(nom, type, objet_Field)". Le troisième
   élément peut être omis. On peut aussi passer un simple nom, auquel
   cas le type sera "typing.Any". Les paramètres restants, à savoir
   *init*, *repr*, *eq*, *order*, *unsafe_hash*, *frozen*,
   *march_args*, *kw_only* et *slots*, sont les mêmes que dans
   "dataclass()".

   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 fonction
   "dataclass()", 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 par "changes". Si "obj" n'est pas une classe de
   données, "TypeError" est levée. Si une clé dans "changes" 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 avec "init=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ée "replace()" (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, renvoie "False".

   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)

dataclasses.MISSING

   Une valeur sentinelle pour dénoter l'absence de *default* ou
   *default_factory*.

dataclasses.KW_ONLY

   Une valeur sentinelle utilisée en tant qu'annotation de type.
   Chaque attribut situé après un pseudo-champ ayant pour type la
   constante "KW_ONLY" est marqué comme champ exclusivement nommé.
   Notez que le pseudo-champ ayant pour type la constante "KW_ONLY"
   est par ailleurs complètement ignoré. Par convention le nom "_" est
   utilisé pour l'attribut "KW_ONLY". Les paramètres de la méthode
   "__init__()" exclusivement nommés doivent être spécifiés par leur
   nom lors de l'instanciation de la classe.

   Dans cette exemple "y" et "z" sont marqués comme exclusivement
   nommés

      @dataclass
      class Point:
        x: float
        _: KW_ONLY
        y: float
        z: float

      p = Point(0, y=1.5, z=2.0)

   Une erreur est levée s'il y a plus d'un champ de type "KW_ONLY"
   dans une unique classe de données.

   Nouveau dans la version 3.10.

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 avec "frozen=True".


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 the few 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
==========================

Another 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 = None
       database: InitVar[DatabaseType | None] = 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):


Réarrangement des paramètres exclusivement nommés dans "__init__()"
===================================================================

Lorsque les paramètres requis pour "__init__()" sont préparés, tout
ceux marqués comme exclusivement nommé sont déplacés pour être
positionnés après tous ceux non exclusivement nommés. Ceci est un
prérequis de la façon dont les paramètres exclusivement nommés sont
implémentés en Python : ils sont après les paramètres non
exclusivement nommés.

Dans cet exemple, "Base.y", "Base.w", et "D.t" sont des champs
exclusivement nommés alors que "Base.x" et "D.z" sont des champs
normaux

   @dataclass
   class Base:
       x: Any = 15.0
       _: KW_ONLY
       y: int = 0
       w: int = 1

   @dataclass
   class D(Base):
       z: int = 10
       t: int = field(kw_only=True, default=0)

La méthode "__init__()" générée pour "D" ressemble à

   def __init__(self, x: Any = 15.0, z: int = 10, *, y: int = 0, w: int = 1, t: int = 0):

Les paramètres ont été réarrangés par rapport à leur ordre
d'apparition dans la liste des champs : les paramètres provenant des
attributs normaux sont suivis par les paramètres qui proviennent des
attributs exclusivement nommés.

L'ordre relatif des paramètres exclusivement nommés est conservé par
le réarrangement des paramètres d'"__init__()".


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__()" (par
   "init=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.x

   Comme attendu, les deux instances de "C" partagent le même objet
   pour l'attribut "x".

   Avec les classes de données, si ce code était valide :

      @dataclass
      class D:
          x: List = []
          def add(self, element):
              self.x += element

   il 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().x

   On se retrouve avec le même problème qu'au premier exemple avec la
   classe "C". Les classes de données étant créées comme toutes les
   autres classes Python, leur comportement est identique. Ainsi, deux
   instances distinctes de "D" où l'attribut "x" a été laissé à sa
   valeur par défaut partagent la même copie de l'objet "x". Il n'y a
   aucun moyen commun de détecter cette situation. C'est pourquoi le
   décorateur "dataclass()" lève "TypeError" si une valeur par défaut
   est de type "list", "dict" ou "set" est détectée. Cette solution
   n'est pas parfaite, mais permet d'éviter la majorité des erreurs.

   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


Descriptor-typed fields
=======================

Fields that are assigned descriptor objects as their default value
have the following special behaviors:

* The value for the field passed to the dataclass's "__init__" method
  is passed to the descriptor's "__set__" method rather than
  overwriting the descriptor object.

* Similarly, when getting or setting the field, the descriptor's
  "__get__" or "__set__" method is called rather than returning or
  overwriting the descriptor object.

* To determine whether a field contains a default value, "dataclasses"
  will call the descriptor's "__get__" method using its class access
  form (i.e. "descriptor.__get__(obj=None, type=cls)".  If the
  descriptor returns a value in this case, it will be used as the
  field's default. On the other hand, if the descriptor raises
  "AttributeError" in this situation, no default value will be
  provided for the field.

   class IntConversionDescriptor:
     def __init__(self, *, default):
       self._default = default

     def __set_name__(self, owner, name):
       self._name = "_" + name

     def __get__(self, obj, type):
       if obj is None:
         return self._default

       return getattr(obj, self._name, self._default)

     def __set__(self, obj, value):
       setattr(obj, self._name, int(value))

   @dataclass
   class InventoryItem:
     quantity_on_hand: IntConversionDescriptor = IntConversionDescriptor(default=100)

   i = InventoryItem()
   print(i.quantity_on_hand)   # 100
   i.quantity_on_hand = 2.5    # calls __set__ with 2.5
   print(i.quantity_on_hand)   # 2

Note that if a field is annotated with a descriptor type, but is not
assigned a descriptor object as its default value, the field will act
like a normal field.
