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, weakref_slot=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, weakref_slot=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: If true (the default value is False), then all fields will be marked as keyword-only. If a field is marked as keyword-only, then the only effect is that the __init__() parameter generated from a keyword-only field must be specified with a keyword when __init__() is called. There is no effect on any other aspect of dataclasses. See the parameter glossary entry for details. Also see the KW_ONLY section.

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.

Modifié dans la version 3.11: If a field name is already included in the __slots__ of a base class, it will not be included in the generated __slots__ to prevent overriding them. Therefore, do not use __slots__ to retrieve the field names of a dataclass. Use fields() instead. To be able to determine inherited slots, base class __slots__ may be any iterable, but not an iterator.

  • weakref_slot: If true (the default is False), add a slot named "__weakref__", which is required to make an instance weakref-able. It is an error to specify weakref_slot=True without also specifying slots=True.

Nouveau dans la version 3.11.

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)

Converts the dataclass obj to a dict (by using the factory function dict_factory). Each dataclass is converted to a dict of its fields, as name: value pairs. dataclasses, dicts, lists, and tuples are recursed into. Other objects are copied with copy.deepcopy().

Example of using asdict() on nested dataclasses:

@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}]}

To create a shallow copy, the following workaround may be used:

dict((field.name, getattr(obj, field.name)) for field in fields(obj))

asdict() raises TypeError if obj is not a dataclass instance.

dataclasses.astuple(obj, *, tuple_factory=tuple)

Converts the dataclass obj to a tuple (by using the factory function tuple_factory). Each dataclass is converted to a tuple of its field values. dataclasses, dicts, lists, and tuples are recursed into. Other objects are copied with copy.deepcopy().

Pour continuer l'exemple précédent :

assert astuple(p) == (10, 20)
assert astuple(c) == ([(0, 0), (10, 4)],)

To create a shallow copy, the following workaround may be used:

tuple(getattr(obj, field.name) for field in dataclasses.fields(obj))

astuple() raises TypeError if obj is not a dataclass instance.

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, weakref_slot=False)

Creates a new dataclass with name cls_name, fields as defined in fields, base classes as given in bases, and initialized with a namespace as given in namespace. fields is an iterable whose elements are each either name, (name, type), or (name, type, Field). If just name is supplied, typing.Any is used for type. The values of init, repr, eq, order, unsafe_hash, frozen, match_args, kw_only, slots, and weakref_slot have the same meaning as they do in 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)

Creates a new object of the same type as obj, replacing fields with values from changes. If obj is not a Data Class, raises TypeError. If values in changes do not specify fields, raises TypeError.

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

Voici le premier des deux cas où le type fourni dans l'annotation d'un champ est pris en compte par dataclass() au moment de l'exécution. Si ce type est typing.ClassVar, le champ est considéré comme une variable de classe, notion définie dans la PEP 526. Les champs de ce genre sont complètement ignorés du point de vue des classes de données. Ils n'apparaissent pas dans le résultat de fields().

Variables d'initialisation

L'autre cas où l'annotation de type influence dataclass() est le suivant : si le type est dataclasses.InitVar, le nom devient un « champ d'initialisation ». Puisque ce n'est pas un vrai champ, il n'est pas inclus dans le n-uplet que renvoie fields(). Les champs d'initialisation sont ajoutés en tant que paramètres à la méthode __init__() générée. Leurs valeurs sont passées à __post_init__(), si cette méthode existe. C'est la seule manière dont elles sont utilisées.

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

This has the same issue as the original example using class C. That is, two instances of class D that do not specify a value for x when creating a class instance will share the same copy of x. 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, the dataclass() decorator will raise a TypeError if it detects an unhashable default parameter. The assumption is that if a value is unhashable, it is mutable. 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

Modifié dans la version 3.11: Instead of looking for and disallowing objects of type list, dict, or set, unhashable objects are now not allowed as default values. Unhashability is used to approximate mutability.

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.