[PYTHON] Expliquer le mécanisme de la classe de données PEP557

TL;DR

Qu'est-ce que la «classe de données»?

dataclass est une nouvelle bibliothèque standard ajoutée dans python 3.7. Pour expliquer brièvement, si vous ajoutez un décorateur @ dataclass à la déclaration de classe, c'est un soi-disant dunder (abréviation de double under score. En japonais, il se lit comme un dunder. ) Une bibliothèque qui génère des méthodes. Il peut être utilisé pour réduire considérablement les définitions de classe fastidieuses et est plus rapide que les implémentations médiocres. Dataclass a diverses fonctions autres que celles présentées ici, donc pour plus de détails, voir Official Document et [Python 3.7] Les «classes de données» peuvent devenir la norme pour les définitions de classe](https://qiita.com/tag1216/items/13b032348c893667862a).

Pour ceux qui ne peuvent toujours pas utiliser python3.7, PyPI a un backport pour 3.6.

Comment utiliser la classe de données

from dataclasses import dataclass, field
from typing import ClassVar, List, Dict, Tuple
import copy

@dataclass
class Foo:
    i: int
    s: str
    f: float
    t: Tuple[int, str, float, bool]
    d: Dict[int, str]
    b: bool = False  #Valeur par défaut
    l: List[str] = field(default_factory=list)  #par défaut pour la liste[]À
    c: ClassVar[int] = 10  #Variable de classe

#Généré`__init__`Instancié avec
f = Foo(i=10, s='hoge', f=100.0, b=True,
        l=['a', 'b', 'c'], d={'a': 10, 'b': 20},
        t=(10, 'hoge', 100.0, False))

#Généré`__repr__`Imprimez la représentation sous forme de chaîne de h avec
print(f)

#Faire une copie et réécrire
ff = copy.deepcopy(f)
ff.l.append('d')

#Généré`__eq__`Comparer avec
assert f != ff

performance

J'ai mesuré le temps d'exécution de DataclassFoo créé en utilisant dataclass et ManualFoo écrit à la main, __init __, __repr__, __eq__.

Code source utilisé pour la mesure
import timeit
from dataclasses import dataclass

@dataclass
class DataclassFoo:
    i: int
    s: str
    f: float
    b: bool

class ManualFoo:
    def __init__(self, i, s, f, b):
        self.i = i
        self.s = s
        self.f = f
        self.b = b
    def __repr__(self):
        return f'ManualFoo(i={self.i}, s={self.s}, f={self.f}, b={self.b})'
    def __eq__(self, b):
        a = self
        return a.i == b.i and a.s == b.s and a.f == b.f and a.b == b.b

def bench(name, f):
    times = timeit.repeat(f, number=100000, repeat=5)
    print(name + ':\t' +  f'{sum(t)/5:.5f}')

bench('dataclass __init__', lambda: DataclassFoo(10, 'foo', 100.0, True))
bench('manual class __init__', lambda: ManualFoo(10, 'foo', 100.0, True))

df = DataclassFoo(10, 'foo', 100.0, True)
mf = ManualFoo(10, 'foo', 100.0, True)
bench('dataclass __repr__', lambda: str(df))
bench('manual class __repr__', lambda: str(mf))

df2 = DataclassFoo(10, 'foo', 100.0, True)
mf2 = ManualFoo(10, 'foo', 100.0, True)
bench('dataclass __eq__', lambda: df == df2)
bench('manual class __eq__', lambda: mf == mf2)

Moyenne de 5 séries de 100 000 fois chacune

Résultat de la mesure(sec)
dataclass __init__ 0.04382
Classe manuscrite__init__ 0.04003
dataclass __repr__ 0.07527
Classe manuscrite__repr__ 0.08414
dataclass __eq__ 0.04755
Classe manuscrite__eq__ 0.04593

On peut dire qu'il n'y a presque aucune différence si elle est exécutée 500 000 fois.

Les codes d'octet correspondaient également.

<détails>

dataclass \ _ \ _ init \ _ \ _ </ b> </ summary>

>>> import dis
>>> dis.dis(DataclassFoo.__init__)
  2           0 LOAD_FAST                1 (i)
              2 LOAD_FAST                0 (self)
              4 STORE_ATTR               0 (i)

  3           6 LOAD_FAST                2 (s)
              8 LOAD_FAST                0 (self)
             10 STORE_ATTR               1 (s)

  4          12 LOAD_FAST                3 (f)
             14 LOAD_FAST                0 (self)
             16 STORE_ATTR               2 (f)

  5          18 LOAD_FAST                4 (b)
             20 LOAD_FAST                0 (self)
             22 STORE_ATTR               3 (b)
             24 LOAD_CONST               0 (None)
             26 RETURN_VALUE

<détails>

Classe manuscrite \ _ \ _init \ _ \ _ </ b> </ summary>

>>> dis.dis(ManualFoo.__init__)
 13           0 LOAD_FAST                1 (i)
              2 LOAD_FAST                0 (self)
              4 STORE_ATTR               0 (i)

 14           6 LOAD_FAST                2 (s)
              8 LOAD_FAST                0 (self)
             10 STORE_ATTR               1 (s)

 15          12 LOAD_FAST                3 (f)
             14 LOAD_FAST                0 (self)
             16 STORE_ATTR               2 (f)

 16          18 LOAD_FAST                4 (b)
             20 LOAD_FAST                0 (self)
             22 STORE_ATTR               3 (b)
             24 LOAD_CONST               0 (None)
             26 RETURN_VALUE

Avant d'entrer dans l'explication interne de la classe de données

Je voudrais expliquer les parties importantes lors de l'explication de la classe de données.

PEP526: Syntax for Variable Annotations

PEP526 décrit la méthode de déclaration de type, mais les informations de type de la variable déclarée dans la classe par cet ajout de spécification Il est maintenant possible de l'obtenir lorsque le programme est exécuté.

from typing import Dict
class Player:
    players: Dict[str, Player]
    __points: int

print(Player.__annotations__)
# {'players': typing.Dict[str, __main__.Player],
#  '_Player__points': <class 'int'>}

Fonction ʻexec` intégrée

Je pense que beaucoup de gens connaissent eval. En gros, la différence avec eval est

ʻEval: évalue la chaîne d'arguments comme une expression ʻExec: évalue la chaîne d'arguments comme une instruction

Cela seul n'a pas de sens, alors regardons l'exemple suivant.

Il est facile d'imaginer que cela produira "des pierres de frappe!".

>>> exec('print("typing rocks!")')
"typing rocks!"

Alors qu'est-ce que c'est?

exec('''
def func():
    print("typing rocks!")
''')

Alors essayez ceci

>>> func()
"typing rocks!"

alors. En fait, exec évalue une chaîne comme une expression, de sorte que même les fonctions python peuvent être définies dynamiquement. Génial.

Alors, que fait la classe de données en interne?

Lorsqu'une classe avec un décorateur de classe de données est importée, le code est généré à l'aide des annotations de type et de l'exécution expliquées ci-dessus. C'est super rugueux, mais le flux est le suivant. Pour plus d'informations, lisez Cette zone de la source cpython.

  1. Le décorateur de classe de données est appelé pour la classe
  2. Obtenez les informations de type (nom du type, classe de type, valeur par défaut, etc.) de chaque champ à partir des annotations de type
  3. Créez une définition de fonction __init__ ** chaîne ** en utilisant les informations de type
  4. Passez la chaîne à ʻexec` pour générer dynamiquement la fonction
  5. Définissez la fonction __init __ dans la classe

Le code qui simplifie 3, 4 et 5 ressemble à ceci.

nl = '\n'  # f-Puisque l'échappement ne peut pas être utilisé à l'intérieur d'une chaîne, définissez-le à l'extérieur

#Création d'une chaîne de définition de fonction
s = f"""
def func(self, {', '.join([f.name for f in fields(Hoge)])}):
{nl.join('  self.'+f.name+'='+f.name for f in fields(Hoge))}
"""

#Essayez de sortir la chaîne de caractères de définition de fonction vers la console
print(s)
# def func(self, i, s, f, t, d, b, l):
#   self.i=i
#   self.s=s
#   self.f=f
#   self.t=t
#   self.d=d
#   self.b=b
#   self.l=l

#Génération de code avec exec.`func`Fonction définie dans le périmètre
exec(s)

setattr(Foo, 'func', func)  #Définit la fonction générée dans la classe dans la classe

Ce qui précède est un exemple simplifié, mais en réalité

  • Valeur par défaut définie pour le champ
  • Fonction d'usine par défaut utilisée pour la liste, etc.
  • ClasseVar
  • Ne pas générer si le programmeur l'a défini
  • Génération d'autres fonctions dunder
  • Héritage de la classe de la classe de données

En considération de tout ce qui précède, la chaîne de caractères de définition de fonction est créée et le code est généré avec soin afin qu'il fonctionne correctement dans tous les cas.

Une autre chose à garder à l'esprit est que cette ** génération de code n'a lieu qu'au moment où le module est chargé **. Une fois la classe importée, elle peut être utilisée ** comme une classe manuscrite **.

Rust's # [derive]

Rust a un attribut Derive (# [derive]) qui est ajouté lors de la définition d'une structure. Cela peut être à peu près identique ou supérieur à la classe de données. Par exemple, si vous regardez ce qui suit,

#[derive(Debug, Clone, Eq, PartialEq, Hash)]
struct Foo {
    i: i32,
    s: String,
    b: bool,
}

Ajoutez simplement # [derive (Debug, Clone, Eq, PartialEq, Hash)] et cela générera autant de méthodes.

  • Génération de méthode pour la génération de chaînes de caractères de débogage (__repr__ en Python)
  • Générer une méthode pour cloner un objet
  • Génération de méthodes de comparaison (__eq__ et __gt__ en Python)
  • Génération de méthode Hasher (__hash__ en Python)

Rust est encore plus incroyable, avec la possibilité d'implémenter votre propre dérivé personnalisé officiellement pris en charge, ce qui le rend relativement décontracté. Permet la métaprogrammation basée sur le type.

Il existe de nombreuses autres fonctionnalités dans Rust qui facilitent ces programmeurs, et je pense que c'est pourquoi Rust est si productif, même avec des contraintes de type et de propriété difficiles. Rust est un très bon langage, j'encourage donc les pythonistes à l'essayer.

Possibilité de dataclass comme métaprogrammation

Je pense personnellement que la classe de données est un bon exemple de l'utilité et du potentiel de la métaprogrammation basée sur le type.

J'ai également créé environ deux bibliothèques basées sur des classes de données, donc si vous êtes intéressé, jetez un œil.

Une bibliothèque qui mappe les valeurs des variables d'environnement aux champs de la classe de données. Utile lorsque vous souhaitez remplacer la classe de configuration Python par une variable d'environnement à l'aide d'un conteneur.

Une bibliothèque de sérialisation basée sur les classes de données. En cours de développement pour implémenter la même fonction que la bibliothèque God de Rust serde en utilisant dataclass.

en conclusion

Comme pour Rust, j'espère que Python sera enthousiasmé par ce domaine et proposera de nombreuses bonnes bibliothèques.

Recommended Posts