[PYTHON] Erläutern Sie den Mechanismus der PEP557-Datenklasse

TL;DR

Was ist eine Datenklasse?

dataclass ist eine neue Standardbibliothek, die in Python 3.7 hinzugefügt wurde. Um es kurz zu erklären: Wenn Sie der Klassendeklaration einen "@ dataclass" -Dekorator hinzufügen, handelt es sich um einen sogenannten Dunder (Abkürzung für "double under score". Auf Japanisch wird er als "dunder" gelesen.) ) Eine Bibliothek, die Methoden generiert. Es kann verwendet werden, um langwierige Klassendefinitionen erheblich zu reduzieren und ist schneller als schlechte Implementierungen. Die Datenklasse hat verschiedene andere als die hier vorgestellten Funktionen. Weitere Informationen finden Sie unter Offizielles Dokument und [Python 3.7]. "Datenklassen" können zum Standard für Klassendefinitionen werden](https://qiita.com/tag1216/items/13b032348c893667862a).

Für diejenigen, die python3.7 immer noch nicht verwenden können, hat PyPI einen Backport für 3.6.

Verwendung von "Datenklasse"

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  #Standardwert
    l: List[str] = field(default_factory=list)  #Standard für Liste[]Zu
    c: ClassVar[int] = 10  #Klassenvariable

#Generiert`__init__`Instanziiert mit
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))

#Generiert`__repr__`Drucken Sie die Zeichenfolgendarstellung von h mit aus
print(f)

#Erstellen Sie eine Kopie und schreiben Sie neu
ff = copy.deepcopy(f)
ff.l.append('d')

#Generiert`__eq__`Vergleichen mit
assert f != ff

Performance

Ich habe die Ausführungszeit von DataclassFoo gemessen, das mit der von Hand geschriebenen Datenklasse und ManualFoo erstellt wurde: "init", "repr", "eq".

Quellcode für die Messung
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)

Durchschnittlich 5 Sätze zu je 100.000 Mal ausführen

Messergebnis(sec)
dataclass __init__ 0.04382
Handschriftliche Klasse__init__ 0.04003
dataclass __repr__ 0.07527
Handschriftliche Klasse__repr__ 0.08414
dataclass __eq__ 0.04755
Handschriftliche Klasse__eq__ 0.04593

Es kann gesagt werden, dass es fast keinen Unterschied gibt, wenn es 500.000 Mal ausgeführt wird.

Auch die Bytecodes stimmten überein.

Datenklasse \ _ \ _ init \ _ \ _
>>> 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
Handschriftliche Klasse \ _ \ _ init \ _ \ _
>>> 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

Bevor wir auf die interne Erklärung der Datenklasse eingehen

Ich möchte die wichtigen Teile bei der Erläuterung der Datenklasse erläutern.

PEP526: Syntax for Variable Annotations

PEP526 beschreibt die Typdeklarationsmethode, jedoch die Typinformationen der Variablen, die durch diesen Spezifikationszusatz in der Klasse deklariert wurden Es ist jetzt möglich, es zu erhalten, wenn das Programm ausgeführt wird.

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'>}

Eingebaute "exec" -Funktion

Ich denke, viele Leute kennen eval. Grob gesagt ist der Unterschied zur Bewertung

eval: Wertet die Argumentzeichenfolge als Ausdruck aus exec: Wertet die Argumentzeichenfolge als Anweisung aus

Dies allein macht keinen Sinn. Schauen wir uns also das nächste Beispiel an.

Es ist leicht vorstellbar, dass dies "Typing Rocks!" Ausgibt.

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

Was ist das dann?

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

Dann versuchen Sie dies

>>> func()
"typing rocks!"

damit. Tatsächlich wertet exec eine Zeichenfolge als Ausdruck aus, sodass sogar Python-Funktionen dynamisch definiert werden können. Toll.

Was macht die Datenklasse intern?

Wenn eine Klasse mit einem Datenklassendekorator importiert wird, wird der Code mithilfe der oben erläuterten Typanmerkungen und exec generiert. Es ist super rau, aber der Fluss ist wie folgt. Weitere Informationen finden Sie unter Dieser Bereich der Cpython-Quelle.

  1. Der Datenklassendekorateur wird für die Klasse aufgerufen
  2. Rufen Sie die Typinformationen (Typname, Typklasse, Standardwert usw.) jedes Felds aus Typanmerkungen ab
  3. Erstellen Sie eine __init__ Funktionsdefinition ** Zeichenfolge ** unter Verwendung von Typinformationen
  4. Übergeben Sie die Zeichenfolge an "exec", um die Funktion dynamisch zu generieren
  5. Stellen Sie die Funktion __init __ in der Klasse ein

Der Code, der 3, 4 und 5 vereinfacht, sieht folgendermaßen aus.

nl = '\n'  # f-Sie können nicht innerhalb einer Zeichenfolge entkommen, definieren Sie sie also außerhalb

#Erstellung von Funktionsdefinitionszeichenfolgen
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))}
"""

#Versuchen Sie, die Zeichenfolge für die Funktionsdefinition an die Konsole auszugeben
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

#Codegenerierung mit exec.`func`Im Geltungsbereich definierte Funktion
exec(s)

setattr(Foo, 'func', func)  #Legen Sie die in der Klasse generierte Funktion in der Klasse fest

Das Obige ist ein vereinfachtes Beispiel, aber in Wirklichkeit

  • Standardwert für das Feld festgelegt
  • Standardmäßige Werksfunktion für Liste usw.
  • ClassVar
  • Nicht generieren, wenn der Programmierer dies definiert hat
  • Erzeugung anderer Dunder-Funktionen
  • Vererbung der Klasse der Datenklasse

In Anbetracht all dieser Punkte wird die Funktionsdefinitionszeichenfolge erstellt und der Code sorgfältig generiert, damit er in jedem Fall ordnungsgemäß funktioniert.

Beachten Sie auch, dass diese ** Codegenerierung nur in dem Moment erfolgt, in dem das Modul geladen wird **. Sobald die Klasse importiert wurde, kann sie ** genau wie eine handschriftliche Klasse ** verwendet werden.

Rusts # [ableiten]

Rust hat ein Derive-Attribut (# [derive]), das beim Definieren einer Struktur hinzugefügt wird. Dies kann ungefähr gleich oder besser als die Datenklasse sein. Wenn Sie sich beispielsweise Folgendes ansehen:

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

Fügen Sie einfach # [ableiten (Debug, Clone, Eq, PartialEq, Hash)] hinzu und es werden so viele Methoden generiert.

  • Methodengenerierung für die Debug-Zeichenfolgengenerierung (__repr__ in Python)
  • Generieren Sie eine Methode zum Klonen eines Objekts
  • Generierung von Vergleichsmethoden (__eq__ und __gt__ in Python)
  • Generierung von Hasher-Methoden (__hash__ in Python)

Rust ist noch erstaunlicher, da Sie Ihr eigenes benutzerdefiniertes Derivat offiziell unterstützt implementieren können, was es relativ lässig macht. Ermöglicht typbasierte Metaprogrammierung.

Es gibt viele andere Funktionen in Rust, die diese Programmierer einfacher machen, und ich denke, deshalb ist Rust selbst bei schwierigen Typbeschränkungen und Besitzverhältnissen so produktiv. Rust ist eine wirklich großartige Sprache, daher ermutige ich Pythonistas, sie auszuprobieren.

Möglichkeit der Datenklasse als Metaprogrammierung

Ich persönlich denke, dass die Datenklasse ein gutes Beispiel für die Nützlichkeit und das Potenzial der typbasierten Metaprogrammierung ist.

Ich habe auch ungefähr zwei Bibliotheken basierend auf Datenklassen erstellt. Wenn Sie also interessiert sind, schauen Sie bitte.

Eine Bibliothek, die die Werte von Umgebungsvariablen Feldern in der Datenklasse zuordnet. Nützlich, wenn Sie die Python-Konfigurationsklasse mithilfe eines Containers mit einer Umgebungsvariablen überschreiben möchten.

Eine auf Datenklassen basierende Serialisierungsbibliothek. In der Entwicklung, um dieselbe Funktion wie Rusts God Library serde unter Verwendung einer Datenklasse zu implementieren.

abschließend

Wie bei Rust hoffe ich, dass Python von diesem Bereich begeistert ist und viele gute Bibliotheken entwickelt.

Recommended Posts