L'une des nouvelles fonctionnalités introduites dans Python 3.7 est la classe Data. Ils sont conçus pour automatiser la génération de code pour les classes utilisées pour stocker des données. Malgré le fait qu'ils utilisent d'autres mécanismes de travail, ils peuvent être comparés à des "tuples nommés mutables avec des valeurs par défaut".
Présentation
Tous les exemples ci-dessus nécessitent Python 3.7 ou supérieur pour leur fonctionnement.
La plupart des développeurs python doivent écrire ces classes régulièrement:
class RegularBook: def __init__(self, title, author): self.title = title self.author = author
Déjà dans cet exemple, la redondance est visible. Les identificateurs de titre et d'auteur sont utilisés plusieurs fois. La classe réelle contiendra également les méthodes remplacées __eq__
et __repr__
.
Le module dataclasses
contient le décorateur @dataclass
. En l'utilisant, un code similaire ressemblerait Ă ceci:
from dataclasses import dataclass @dataclass class Book: title: str author: str
Il est important de noter que des annotations de type sont requises . Tous les champs qui n'ont pas de marques de type seront ignorés. Bien sûr, si vous ne souhaitez pas utiliser un type spécifique, vous pouvez spécifier Any
dans le module de typing
.
Qu'obtenez-vous en conséquence? Vous obtenez automatiquement une classe, avec les méthodes implémentées __init__
, __repr__
, __str__
et __eq__
. De plus, ce sera une classe régulière et vous pouvez en hériter ou ajouter des méthodes arbitraires.
>>> book = Book(title="Fahrenheit 451", author="Bradbury") >>> book Book(title='Fahrenheit 451', author='Bradbury') >>> book.author 'Bradbury' >>> other = Book("Fahrenheit 451", "Bradbury") >>> book == other True
Alternatives
Tuple ou dictionnaire
Bien sûr, si la structure est assez simple, vous pouvez enregistrer les données dans un dictionnaire ou un tuple:
book = ("Fahrenheit 451", "Bradbury") other = {'title': 'Fahrenheit 451', 'author': 'Bradbury'}
Cependant, cette approche présente des inconvénients:
- Il faut se rappeler que la variable contient des données liées à cette structure.
- Dans le cas d'un dictionnaire, vous devez garder une trace des noms des clés. Une telle initialisation du dictionnaire
{'name': 'Fahrenheit 451', 'author': 'Bradbury'}
sera également formellement correcte. - Dans le cas d'un tuple, vous devez garder une trace de l'ordre des valeurs, car elles n'ont pas de nom.
Il y a une meilleure option:
Namedtuple
from collections import namedtuple NamedTupleBook = namedtuple("NamedTupleBook", ["title", "author"])
Si nous utilisons la classe ainsi créée, nous obtenons pratiquement la même chose que l'utilisation de la classe de données.
>>> book = NamedTupleBook("Fahrenheit 451", "Bradbury") >>> book.author 'Bradbury' >>> book NamedTupleBook(title='Fahrenheit 451', author='Bradbury') >>> book == NamedTupleBook("Fahrenheit 451", "Bradbury")) True
Mais malgré la similitude générale, les tuples nommés ont leurs limites. Ils viennent du fait que les tuples nommés sont toujours des tuples.
Tout d'abord, vous pouvez toujours comparer les instances de différentes classes.
>>> Car = namedtuple("Car", ["model", "owner"]) >>> book = NamedTupleBook("Fahrenheit 451", "Bradbury")) >>> book == Car("Fahrenheit 451", "Bradbury") True
Deuxièmement, les tuples nommés sont immuables. Dans certaines situations, cela est utile, mais j'aimerais plus de flexibilité.
Enfin, vous pouvez opérer sur un tuple nommé aussi bien qu'un régulier. Par exemple, répétez.
Autres projets
S'il n'est pas limité à la bibliothèque standard, vous pouvez trouver d'autres solutions à ce problème. En particulier, le projet attire . Il peut faire encore plus que la classe de données et fonctionne sur les anciennes versions de python telles que 2.7 et 3.4. Néanmoins, le fait qu'il ne fasse pas partie de la bibliothèque standard peut être gênant
La création
Vous pouvez utiliser le décorateur @dataclass
pour créer une classe de données. Dans ce cas, tous les champs de la classe définis avec l'annotation de type seront utilisés dans les méthodes correspondantes de la classe résultante.
Comme alternative, il existe la fonction make_dataclass
, qui fonctionne de manière similaire à la création de tuples nommés.
from dataclasses import make_dataclass Book = make_dataclass("Book", ["title", "author"]) book = Book("Fahrenheit 451", "Bradbury")
Valeurs par défaut
Une fonctionnalité utile est la facilité d'ajouter des valeurs par défaut aux champs. Il n'est toujours pas nécessaire de redéfinir la méthode __init__
, il suffit de spécifier les valeurs directement dans la classe.
@dataclass class Book: title: str = "Unknown" author: str = "Unknown author"
Ils seront pris en compte dans la méthode __init__
générée
>>> Book() Book(title='Unknown', author='Unknown author') >>> Book("Farenheit 451") Book(title='Farenheit 451', author='Unknown author')
Mais comme pour les classes et méthodes normales, vous devez être prudent en utilisant les valeurs par défaut modifiables. Si, par exemple, vous devez utiliser la liste comme valeur par défaut, il existe un autre moyen, mais plus à ce sujet ci-dessous.
De plus, il est important de surveiller l'ordre dans lequel les champs avec des valeurs par défaut sont déterminés, car il correspond exactement à leur ordre dans la méthode __init__
Classes de données immuables
Les instances de tuples nommés sont immuables. Dans de nombreuses situations, c'est une bonne idée. Pour les classes de données, vous pouvez également le faire. Spécifiez simplement le paramètre FrozenInstanceError
frozen=True
lors de la création de la classe, et si vous essayez de modifier ses champs, une exception FrozenInstanceError
sera FrozenInstanceError
@dataclass(frozen=True) class Book: title: str author: str
>>> book = Book("Fahrenheit 451", "Bradbury") >>> book.title = "1984" dataclasses.FrozenInstanceError: cannot assign to field 'title'
Paramètre de classe de données
En plus du paramètre frozen
, le décorateur @dataclass
a d'autres paramètres:
init
: si elle est True
(par défaut), la méthode __init__
est générée. Si la classe a déjà une méthode __init__
définie, le paramètre est ignoré.repr
: active (par défaut) la création de la méthode __repr__
. La chaîne générée contient le nom de la classe ainsi que le nom et la représentation de tous les champs définis dans la classe. Dans ce cas, des champs individuels peuvent être exclus (voir ci-dessous)eq
: active (par défaut) la création de la méthode __eq__
. Les objets sont comparés de la même manière que s'ils étaient des tuples contenant les valeurs de champ correspondantes. De plus, la correspondance des types est vérifiée.order
active (par défaut est désactivé) la création des __lt__
, __le__
, __gt__
et __ge__
. Les objets sont comparés de la même manière que les tuples correspondants des valeurs de champ. Dans le même temps, le type d'objets est également vérifié. Si l' order
spécifié, mais pas l' eq
, une exception ValueError
sera levée. De plus, la classe ne doit pas contenir de méthodes de comparaison déjà définies.unsafe_hash
affecte la génération de la méthode __hash__
. Le comportement dépend également des valeurs des paramètres eq
et frozen
Personnaliser des champs individuels
Dans la plupart des situations standard, cela n'est pas obligatoire, mais il est possible de personnaliser le comportement de la classe de données jusqu'aux champs individuels à l'aide de la fonction de champ.
Valeurs par défaut modifiables
Une situation typique mentionnée ci-dessus est l'utilisation de listes ou d'autres valeurs par défaut modifiables. Vous voudrez peut-être une classe «bibliothèque» contenant une liste de livres. Si vous exécutez le code suivant:
@dataclass class Bookshelf: books: List[Book] = []
l'interprète signalera une erreur:
ValueError: mutable default <class 'list'> for field books is not allowed: use default_factory
Cependant, pour d'autres valeurs modifiables, cet avertissement ne fonctionnera pas et entraînera un comportement incorrect du programme.
Pour éviter les problèmes, il est suggéré d'utiliser le paramètre default_factory
de la fonction de field
. Sa valeur peut être n'importe quel objet ou fonction appelé sans paramètres.
La version correcte de la classe ressemble Ă ceci:
@dataclass class Bookshelf: books: List[Book] = field(default_factory=list)
Autres options
En plus de la default_factory
spécifiée, la fonction de champ a les paramètres suivants:
default
: la valeur default
. Ce paramètre est obligatoire car l'appel à field
remplace la valeur de champ par défaut.init
: active (par défaut) l'utilisation d'un champ dans la méthode __init__
repr
: active (par défaut) l'utilisation d'un champ dans la méthode __repr__
compare
inclut (par défaut) l'utilisation du champ dans les méthodes de comparaison ( __eq__
, __le__
et autres)hash
: peut être une valeur booléenne ou None
. S'il est True
, le champ est utilisé pour calculer le hachage. Si None
spécifié (par défaut), la valeur du paramètre de compare
est utilisée.
L'une des raisons de spécifier hash=False
pour une compare=True
donnée compare=True
peut être la difficulté de calculer le hachage de champ alors qu'il est nécessaire pour la comparaison.metadata
: dictionnaire personnalisé ou None
. La valeur est MappingProxyType
dans MappingProxyType
afin qu'elle devienne immuable. Ce paramètre n'est pas utilisé par les classes de données elles-mêmes et est destiné aux extensions tierces.
Traitement après l'initialisation
La méthode __init__
générée __init__
appelle la méthode __post_init__
, si elle est définie dans la classe. En règle générale, il est appelé sous la forme self.__post_init__()
, cependant, si des variables de type InitVar
définies dans la classe, elles seront transmises en tant que paramètres de méthode.
Si la méthode __init__
n'a pas été générée, alors __post_init__
ne sera pas appelé.
Par exemple, ajoutez une description de livre générée
@dataclass class Book: title: str author: str desc: str = None def __post_init__(self): self.desc = self.desc or "`%s` by %s" % (self.title, self.author)
>>> Book("Fareneheit 481", "Bradbury") Book(title='Fareneheit 481', author='Bradbury', desc='`Fareneheit 481` by Bradbury')
Paramètres pour l'initialisation uniquement
L'une des possibilités associées à la méthode __post_init__
est les paramètres utilisés uniquement pour l'initialisation. Si, lors de la déclaration d'un champ, spécifiez InitVar
comme type, sa valeur sera transmise en tant que paramètre de la méthode __post_init__
. En aucun cas, ces champs ne sont utilisés dans la classe de données.
@dataclass class Book: title: str author: str gen_desc: InitVar[bool] = True desc: str = None def __post_init__(self, gen_desc: str): if gen_desc and self.desc is None: self.desc = "`%s` by %s" % (self.title, self.author)
>>> Book("Fareneheit 481", "Bradbury") Book(title='Fareneheit 481', author='Bradbury', desc='`Fareneheit 481` by Bradbury') >>> Book("Fareneheit 481", "Bradbury", gen_desc=False) Book(title='Fareneheit 481', author='Bradbury', desc=None)
Héritage
Lorsque vous utilisez le décorateur @dataclass
, il parcourt toutes les classes parentes en commençant par objet et pour chaque classe de données trouvée, il enregistre les champs dans un dictionnaire ordonné, puis en ajoutant les propriétés de la classe traitée. Toutes les méthodes générées utilisent des champs du dictionnaire ordonné résultant.
Par conséquent, si la classe parente définit des valeurs par défaut, vous devrez définir les champs avec des valeurs par défaut.
Étant donné que le dictionnaire ordonné stocke les valeurs dans l'ordre d'insertion, pour les classes suivantes
@dataclass class BaseBook: title: Any = None author: str = None @dataclass class Book(BaseBook): desc: str = None title: str = "Unknown"
une méthode __init__
avec cette signature sera générée:
def __init__(self, title: str="Unknown", author: str=None, desc: str=None)