Introduction aux classes de données

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) 

Source: https://habr.com/ru/post/fr415829/


All Articles