Une exploration des avantages de @dataclass
Mon premier langage était FORTRAN, qui est plus ou moins inutilisable pour le calcul distribué à cause des variables globales. J’ai juré à l’époque d’éviter les variables globales si je le pouvais.
J’ai adopté l’orienté objet (LOOPS — Lisp Library) pour sa capacité à encapsuler les états des données. Le passage de toutes les données encapsulées a été accompli en passant l’instance d’objet (en mémoire.)
Pendant trente ans, j’ai utilisé des objets ou des classes dans toutes les langues. J’ai peur des héritages multiples, car il est difficile à lire et à refactoriser (pour les humains).
Les avantages que nous avons réalisés en utilisant Python @dataclass
- La révision du code des classes prend maintenant environ la moitié du temps.
- En moyenne, une ligne de déclaration d’argument
@dataclass
code remplace quinze lignes de code. - Le code défectueux (bogues), mesuré par le temps nécessaire pour produire du code prêt pour la production, a été réduit d’environ 8 %.
Comment expliquons-nous l’amélioration en utilisant Python @classededonnées?
Il existe de nombreuses raisons pour lesquelles nous avons une augmentation de l’efficacité du programmeur et de la qualité du code en utilisant @dataclass
. Voici une liste des améliorations majeures importantes que nous avons connues :
- La validation des données:
@dataclass
génère automatiquement des méthodes pour vérifier la validité des données dans une instance de classe.@dataclass
garantit que les données d’une instance de classe sont au format correct et respectent toutes les contraintes spécifiées. - Lisibilité améliorée :
@dataclass
améliore la compréhension de la structure et de l’objectif d’une classe en séparant clairement les champs de données du comportement. - Génération automatique du code passe-partout :
@dataclass
décorateur génère plusieurs méthodes, telles que__init__
,__repr__
et__eq__
souvent nécessaire lorsque vous travaillez avec des classes de données. - Maintenabilité améliorée :
@dataclass
promouvoir une meilleure maintenance et réduire le coût de la refactorisation du code en fournissant une séparation claire entre les données et le comportement. - Performances améliorées : Le
@dataclass
decorator génère des méthodes optimisées pour les performances, telles que__init__
qui utilise des arguments de mots clés uniquement pour réduire le nombre de recherches d’attributs nécessaires. - Compatibilité améliorée avec les structures de données immuables : les valeurs de données deviennent immuables à l’aide de la
@dataclass frozen=TRUE
option. Nous montrons, avec un exemple, comment transformer un ensemble de constantes globales en un ensemble partagé de constantes.
Meilleure pratique : n’utilisez jamais « def class » sans @dataclass.
Il y a probablement des cas où vous n’utiliseriez pas @dataclass
avec votre déclaration de classe. Je n’en connais pas (je n’inclus pas le cas d’héritage).
J’utilise une déclaration de classe dans n’importe quel langage pour maintenir l’état des données internes, appelées membres.
Les classes peuvent exister sans état et uniquement avec des méthodes, mais à quoi ça sert ?
Je suppose que vous pourriez avoir une classe sans membres et masquer les noms de fonctions internes et leurs fonctionnalités. Cependant, les espaces de noms le font déjà.
J’essayais de me rappeler si j’avais déjà créé une classe sans état encapsulé (membres de données de classe). Je ne peux pas penser à un.
Les classes existent pour encapsuler l’état (champs de données) et les méthodes qui opèrent sur les champs de données. Je transforme les méthodes en fonctions sans état de données à encapsuler.
Noter: Il y a un cas à faire valoir que vous pouvez masquer les fonctions d’assistance dans une classe. J’ai trouvé dans de nombreuses critiques que cela encourageait à placer des copies de code redondantes au lieu de les réutiliser. Je n’ai toujours pas trouvé de bonne raison de définir une classe sans état de données.
Jusqu’à cette partie du blog, je supposais que vous connaissiez @dataclass
et pourrait être d’accord ou non avec ma déclaration :
N’utilisez jamais la classe def sans
@dataclass
Dans la section suivante, vous trouverez un aperçu rapide de @dataclass
.
Si la section suivante vous laisse insatisfait, je vous recommande ce qui suit :
De nombreux autres articles vous donneront également de bonnes bases.
Noter: Ce qui suit est probablement non-pyhonique à déclarer, mais pourriez-vous imaginer une version Python 4 où
@dataclass
disparaît parce qu’il est fusionné avecdef class.
Noter: Si je me trompe, car j’ai peut-être raté quelque chose, faites-le moi savoir.
Vous vous doutiez probablement déjà que j’utilise @dataclass
pour toutes mes définitions de classe en Python.
Python @dataclass
décore un def class
définition et génère automatiquement les trois méthodes de double dunder __init__()
, __repr__()
et __eq__()
.
Noter: Il en génère d’autres, mais nous y reviendrons plus tard.
Notez que le total des cinq méthodes de double dunder générées par @dataclass
travailler directement avec l’état encapsulé de la classe. @dataclass
élimine le code passe-partout répétitif requis pour définir une classe de base.
Voici un exemple de commeclasse hort dans le pack Photonaie transformé par le @dataclass
:
### Example #1
from inspect import signature, getmembers
from typing import Dict, List, Any
import sys
class Data:
def __init__(self, X: np.ndarray =None, y: np.array=None,
kwargs: Dict =None):
self.X = X
self.y = y
self.kwargs = kwargs
def __repr__(self):
return self.val
def __eq__(self, other):
return self.val == other.val
Après @dataclass
décorateur
from dataclasses import dataclass
@dataclass
class Data:
X: np.ndarray = None # The field declaration: X
y: np.array = None # The field declaration: y
kwargs: Dict = None # The field declaration: kwargs
Noter: Le champ est ignoré si le type ne fait pas partie de cette déclaration. Utilisez le genre Any
pour le type générique, si le type varie ou est inconnu au moment de l’exécution.
A été__eq__()
code généré ?
### Example #2data1 = Data()
data2 = Data()
data1 == data1
Exemple de sortie #2
True
Oui! Que diriez-vous __repr__()
et __str__
méthodes ?
### Example #3print(data1)
data1
Exemple de sortie #3
Data(X=None, y=None, kwargs=None)
Data(X=None, y=None, kwargs=None)
Oui!
Que diriez-vous de la __init__
méthode?
Example #4@dataclass(unsafe_hash=True)
class Data:
X: np.ndarray = None
y: np.array = None
kwargs: Dict = None
data3 = Data(1,2,3)
Exemple de sortie #4
Data(X=1, y=2, kwargs=3)
Oui!
Noter: Le
__init__
la méthode générée a toujours une signature(X, y, kwargs)
. Notez également que l’interpréteur Python 3.7 a ignoré les indications de type.
Voici un exemple plus long de photonai/photonai/base/hyperpipe.py
:
Exemple #5, remplacement def class
avec @dataclass
décorateur
@dataclass
class CrossValidation:
inner_cv: int
outer_cv: int
eval_final_performance: bool = True
test_size: float = 0.2
calculate_metrics_per_fold: bool = True
calculate_metrics_across_folds: bool = False
outer_folds = None
inner_folds = dict()
En utilisant @dataclass
à la place de def class
j’ai augmenté la lisibilité de mon code.
### Example #6cv1 = CrossValidation()
Exemple de sortie #6
inner_cv
et outer_cv
sont des arguments positionnels. Avec n’importe quelle signature, vous déclarez un champ non par défaut après un champ par défaut.
Indice: Si ce qui précède était autorisé, l’héritage d’une classe parente est interrompu. C’était/est une question d’entretien Goggle.
### Example #7cv1 = CrossValidation(1,2)
cv2 = CrossValidation(1,2)
cv3 = CrossValidation(3,2,test_size=0.5)
print(cv1)
cv3
Exemple de sortie #7
Que diriez-vous __eq__
méthodes ?
### Example #8cv1 == cv2
Exemple de sortie #8
True
Dans l’exemple #9, est cv1
et cv3
équivalent?
### Example #9cv1 == cv3
Exemple de sortie #9
False
Non, cv1
et cv3
ne sont pas équivalents ? Oui le __eq__
méthode se comporte correctement.
Le passe-partout explicite @property et @setproperty n’est plus nécessaire
De tous les passe-partout, @dataclass
génère automatiquement l’élimination du besoin de @property
et @setproperty
. C’est mon avantage préféré.
### Example #19@dataclass
class Data():
X: np.ndarray = None # The field declaration: X
y: np.array = None # The field declaration: y
kwargs: Dict = None # The field declaration: kwargs
d = Data()
d.kwargs
Exemple de sortie #19
# nothing output
Encore une fois, en utilisant@dataclass,
J’ai augmenté la lisibilité de mon code.
Voici comment régler kwargs:
### Example #20d.kwargs = {'one':1}
d.kwargs
Exemple de sortie #20
{'one':1}
Notez que Python ignore les indications de type. Voici à quoi cela ressemble :
### Example #21d.kwargs = 1
d.kwargs
Exemple de sortie #21
1
Ouah! Vous pouvez maintenant placer vos globales dans votre classe et les partager dans votre package !
Je dois te prévenir. Presque toutes les constantes universelles sont définies dans le package de classe scipy.constants
. S’il vous plaît, ne réinventez pas la roue.
Cependant, je suis sûr que vous avez vos constantes préférées que vous aimez utiliser dans vos packages Python. Maintenant, vous pouvez accomplir le partage de vos constantes en @dataclass
au lieu de la pratique fastidieuse de copier des globales. Sans oublier que les globals entraînent un coût de maintenance énorme.
Comment déclarez-vous que les constantes ne peuvent pas être mutées?
Si nous voulons créer une classe partagée de constantes de données qui ne peuvent pas être modifiées (modifiées) ultérieurement.
Vous créez une classe avec des membres de données immuables avec le frozen
argument à dataclass:
### Example 25@dataclass(frozen=True)
class Data():
X: np.ndarray = 0 # The field declaration: X
y: np.array = 0 # The field declaration: y
z: int = 0 # The field declaration: kwargs
d = Data()
d.y = 2
Exemple de sortie #25
En cliquant shift-<tab>
dans un cahier Jupyter affiche la signature et la valeur par défaut pour tous les arguments pour @classededonnées.
Le __init__()
, __repr__()
et __eq__()
avoir une valeur par défaut de mot-clé de True tandis que __order__()
,__unsafe_hash__(),
et __frozen_()
avoir une valeur par défaut de mot-clé de False.
Voici le code :
### Example #17@dataclass(order = True)
class Data():
X: np.ndarray = None # The field declaration: X
y: np.array = None # The field declaration: y
kwargs: Dict = None # The field declaration: kwargs
Cela générerait automatiquement ce qui suit :
class Data():
X: np.ndarray = None # The field declaration: X
y: np.array = None # The field declaration: y
kwargs: Dict = None # The field declaration: kwargs
... default autogenerated methods, plus
def __ge__(self, other):
return self.val >= other.val
def __gt__(self, other):
return self.val > other.val
def __le__(self, other):
return self.val <= other.val
def __lt__(self, other):
return self.val < other.val
Tel que :
### Example #18print(data1 > data2)
print(data1 >= data2)
print(data1 < data2)
print(data1 <= data2)
Sortie de l’exemple #18 :
False
True
False
True
Appliquer l’attribution, __slots__
si vous souhaitez accélérer l’accès aux données membres d’une classe.
### Example #22
@dataclass
class LoggingState:
__slots__ = ['debug', 'info', 'success', 'warning', 'error', 'critical']
debug: bool
info: bool
success: bool
warning: bool
error: bool
critical: bool
logg = LoggingState(debug=False, info=False, success=False, warning=True, error=True, critical=True )
je n’utilise pas __slots__
si le profil indique que les instances de classe représentent moins de 10 % de la charge.
Noter: Veuillez tester les performances pour Python 11.x et au-delà comme __slots__
les performances peuvent changer.
j’ajoute une méthode power_args
à @dataclass
de la même manière que def class
. Voici à quoi cela ressemble :
### Example #23@dataclass
class Data():
X: np.ndarray = None # The field declaration: X
y: np.array = None # The field declaration: y
z: int = 0 # The field declaration: kwargs
def power_args(self):
self.z = self.X**self.y
d = Data(1,2)
d.power_args()
d.z
Exemple de sortie #22 :
2
Ouais! La méthode power_args
travaillé.
Encore une fois, voici un autre exemple :
### Example #24d = Data(5,2)
d.power_args()
d.z
Sortie de l’exemple 23 :
25
Encore une fois, la lisibilité du code est améliorée en utilisant @dataclass
.
Un @dataclass
peut être un sous-classe d’un autre @dataclass
. Par exemple, je prolonge Data()
avec le Datatail @dataclass
.
### Example 29@dataclass
class Data():
X: np.ndarray = None
y: np.array = None
kwargs: Dict = None
def __post_init__(self):
self.kwargs = {}
@dataclass
class Datatail(Data):
z: int = 0
d = Datatail()
d
Exemple de sortie #29
Il existe une méthode post-init qui fait partie du @dataclass
définition. Le __post_init__
méthode s’exécute après le __init__
généré par @dataclass.
Il permet le traitement une fois l’état de la signature défini.
Si vous essayez de définir une liste, un tuple ou un Dict sur autre chose que None
entraîne une erreur.
### Example 26@dataclass
class Data():
X: np.ndarray = None # The field declaration: X
y: np.array = None # The field declaration: y
kwargs: Dict = {} # The field declaration: kwargs
Exemple de sortie #26 :
j’utilise __post_init
pour contourner cette limitation. Voici à quoi cela ressemble :
De même, nous pouvons résoudre le problème d’initiation que nous avons rencontré à la fin de la technique 2. Inspecter un @dataclass
Génération de passe-partout de classe def.
Nous terminons la transformation en fixant l’état restant de CrossValidation
avec un post_init
. Voici le code :
### Example 28from dataclasses import dataclass
@dataclass
class CrossValidation:
inner_cv: int = 0
outer_cv: int = 0
eval_final_performance: bool = True
test_size: float = 0.2
calculate_metrics_per_fold: bool = True
calculate_metrics_across_folds: bool = False
def __post_init__(self):
self.outer_folds = dict()
self.inner_folds = dict()
Noter: les indications de type peuvent être utilisées dans __post_init__ .
Exemple de sortie #28
Invoquer help
nous détaillons les données membres et les méthodes de @dataclass Data.
### Example #10help(Data)
Exemple de sortie #10
Sur la figure 5, nous voyons @dataclass
a généré automatiquement les trois méthodes de double dunder : __init__()
, __repr__()
et __eq__()
.
La fonction getmembershelp
détaille les métadonnées de chaque membre (attribut) de la classe Data.
### Example #11getmembers(Data)
Exemple de sortie #11
Nous examinons une classe à l’aide de la bibliothèque inspect. Nous inspectons le data1
méthode avec signature
:
### Example #12from inspect import signature
print(signature(data1.__init__))
### Example #13print(signature(data1.__eq__))
Exemple de sortie #13
(other)
Cool majeur !
### Example #14print(signature(cv1.__init__))
Exemple de sortie #14
Oups ! Qu’en est-il de ce scénario ?
# Example #15@dataclass
class CrossValidation:
inner_cv: int
outer_cv: int
eval_final_performance: bool = True
test_size: float = 0.2
calculate_metrics_per_fold: bool = True
calculate_metrics_across_folds: bool = False
outer_folds = None
inner_folds = dict()
cv1 = CrossValidation(1,2)
cv2 = CrossValidation(1,2)
cv3 = CrossValidation(3,2,test_size=0.5)
print(cv1)
cv3
Exemple de sortie #15
outer_folds
et inner_folds
sont des membres de données mais ne sont pas créés dans la signature d’appel.
Pourquoi pas? J’ai seulement remarqué que je n’avais pas donné d’indice de type.
Attends une minute; cela ne peut pas être la raison. Le type d’interpréteur Python ignore les indications de type. Serait-ce un cas de coin de test manqué?
Peut-être que les versions ultérieures de Python vont changer cela @dataclass
comportement.
Ne vous inquiétez pas, @dataclass
gère ce cas d’utilisation. Je montre comment avec la technique 8. Post-Init Processing.
@dataclass
c’est cool! Source: Unsplash.Nous avons accueilli @dataclass
dans Python 3.7, non pas à cause d’une fonctionnalité mais parce qu’il a généré automatiquement la plupart du passe-partout nécessaire pour définir une classe (objet) avec un état de données. Une deuxième raison proche était l’amélioration de la lisibilité de notre code Python.
J’ai démontré comment avec vingt-neuf exemples de code qui montrent comment @dataclass
transforme les classes. Nous avons vu comment @dataclass
lisibilité considérablement augmentée.
Une meilleure lisibilité rend le code de production vivant plus facile à comprendre pour tous, y compris le programmeur. Vous comprenez mieux les résultats, ce qui conduit à de meilleurs tests, à moins de bogues et à des coûts de maintenance réduits.
Continuez à coder de manière productive ! Continuez à vous amuser!
- Classes de données en Python 3.7+ (Guide)
- Documentation Python @dataclass