Python Dataclass Tutorial

So nutzen Sie Datenklassen in Python

01.02.2023
Von 
Serdar Yegulalp schreibt für unsere US-Schwesterpublikation Infoworld.
Python Dataclasses machen Entwicklern das Leben leichter. Dieses Tutorial vermittelt Ihnen, wie sie Datenklassen in Python einsetzen.
Starten Sie mit Python-Datenklassen durch. Dieser Artikel zeigt Ihnen, wie das geht.
Starten Sie mit Python-Datenklassen durch. Dieser Artikel zeigt Ihnen, wie das geht.
Foto: Dmitry Kovalchuk - shutterstock.com

In Python ist alles ein Objekt - heißt es. Wenn Sie Ihre eigenen benutzerdefinierten Objekte mit eigenen Eigenschaften und Methoden erstellen möchten, nutzen Sie dazu das class-Objekt von Python. In Python Klassen zu erstellen kann jedoch manchmal bedeuten, eine Menge repetitiven, unausgegorenen Codes schreiben zu müssen, um die Klasseninstanz anhand der übergebenen Parameter einzurichten oder um allgemeine Funktionen wie Vergleichsoperatoren zu erstellen.

Eine praktische und weniger langatmige Möglichkeit, Klassen zu erstellen, bieten die in Python 3.7 eingeführten (und auf Python 3.6 rückwirkend portierten) Dataclasses. Sie ermöglichen es, viele Dinge auf einige wenige, grundlegende Anweisungen zu reduzieren.

Python Dataclass - ein Beispiel

Im Folgenden ein einfaches Beispiel für eine herkömmliche Klasse in Python:

class Book:

'''Object for tracking physical books in a collection.'''

def __init__(self, name: str, weight: float, shelf_id:int = 0):

self.name = name

self.weight = weight # in grams, for calculating shipping

self.shelf_id = shelf_id

def __repr__(self):

return(f"Book(name={self.name!r},

weight={self.weight!r}, shelf_id={self.shelf_id!r})")

Das größte Kopfzerbrechen bereitet hierbei die Art und Weise, wie jedes der an __init__ übergebenen Argumente in die Eigenschaften des Objekts kopiert werden muss. Wenn Sie nur mit Book zu tun haben, ist das ist nicht so schlimm - aber was, wenn Bookshelf, Library, Warehouse und so weiter hinzukommen? Je mehr Code Sie von Hand eingeben müssen, desto größer ist die Wahrscheinlichkeit, einen Fehler zu machen.

Nun betrachten wir dieselbe Python-Klasse - allerdings implementiert als Python Dataclass:

from dataclasses import dataclass

@dataclass

class Book:

'''Object for tracking physical books in a collection.'''

name: str

weight: float

shelf_id: int = 0

Wenn Sie Eigenschaften, auch Fields genannt, in einer Datenklasse angeben, generiert der @dataclass-Decorator automatisch den gesamten Code, der für ihre Initialisierung erforderlich ist. Dabei behält er auch die Typinformationen für jede Eigenschaft. Wenn Sie also einen Code-Linter wie mypy verwenden, stellt er sicher, dass Sie dem Klassenkonstruktor die richtigen Arten von Variablen übergeben.

Eine weitere Aufgabe, die @dataclass hinter den Kulissen übernimmt: Sie erstellt automatisch Code für eine Reihe gängiger Dunder-Methoden in der Klasse. In der konventionellen Klasse oben mussten wir unser eigenes __repr__ erstellen. In der Datenklasse erledigt das der @dataclass-Decorator.

Sobald eine Dataclass erstellt ist, ist sie funktional mit einer regulären Klasse identisch. Performance-Einbußen verursacht die Verwendung einer Datenklasse nicht - lediglich bei der Deklaration einer Klasse als Datenklasse gibt es kleinere, allerdings einmalige Einbußen.

Der Dataclass Decorator kann eigene Initialisierungsoptionen fahren. Diese dürften lediglich in bestimmten Randfällen zur Anwendung kommen. Einige der nützlichsten sind (alle True/False):

  • frozen: Erzeugt schreibgeschützte Klasseninstanzen. Sobald die Daten zugewiesen wurden, können sie nicht mehr verändert werden.

  • slots: Ermöglicht es, weniger Speicher für die Instanzen von Dataclasses zu verwenden, indem nur Felder zugelassen werden, die explizit in der Klasse definiert sind.

  • kw_only: Ist diese Option gesetzt, sind alle Felder der Klasse auf Keywords beschränkt.

Python-Datenklassen anpassen

Die Standardfunktionalitäten von Datenklassen sollten für die meisten Anwendungsfälle ausreichen. Manchmal kann es jedoch nötig werden, die Initialisierung der Fields innerhalb der Dataclass feinabzustimmen. Im Folgenden sehen Sie, wie Sie dazu die field-Funktion verwenden:

from dataclasses import dataclass, field

from typing import List

@dataclass

class Book:

'''Object for tracking physical books in a collection.'''

name: str

condition: str = field(compare=False)

weight: float = field(default=0.0, repr=False)

shelf_id: int = 0

chapters: List[str] = field(default_factory=list)

Wenn Sie einer Instanz von field einen Standardwert zuweisen, verändert sich je nach Parameter die Art und Weise, wie das Feld eingerichtet wird. Zu den meistverwendeten field-Optionen gehören unter anderem:

  • default: Legt den Standardwert für das Feld fest. Sie müssen default verwenden, wenn Sie: a) field verwenden, um andere Parameter für das Field zu verändern und b) darüber hinaus einen Standardwert für das Field festlegen wollen. In unserem Fall verwenden wir default, um weight auf 0.0 zu setzen.

  • default_factory: Gibt den Namen einer Funktion an, die keine Parameter benötigt und ein Objekt zurückgibt, das als Standardwert für das Field dient. In unserem Fall soll chapters eine leere Liste sein.

  • repr: Legt standardmäßig (True) fest, ob das betreffende Field in der automatisch generierten __repr__ für die Datenklasse auftaucht. In diesem Fall wollen wir nicht, dass das Gewicht von Book im __repr__ angezeigt wird. Also verwenden wir repr=False.

  • compare: Inkludiert standardmäßig (True) das Field in die automatisch für die Datenklasse generierten Vergleichsmethoden. Weil wir nicht wollen, dass condition als Teil des Vergleichs für zwei Bücher verwendet wird, setzen wir compare=False.

Beachten Sie dabei, dass wir die Reihenfolge der Fields so anpassen müssen, dass die nicht standardmäßigen an erster Stelle stehen.

Python Dataclasses steuern

An dieser Stelle fragen Sie sich vielleicht, wie Sie zu Feinabstimmungszwecken Kontrolle über den Initialisierungsprozess bekommen können, wenn die __init__-Methode einer Dataclass automatisch erzeugt wird.

__post_init__

An dieser Stelle kommt die __post_init__-Methode ins Spiel. Wenn Sie diese in Ihre Dataclass-Definition aufnehmen, können Sie Anweisungen geben, um Fields und andere Instanzdaten zu ändern:

from dataclasses import dataclass, field

from typing import List

@dataclass

class Book:

'''Object for tracking physical books in a collection.'''

name: str

weight: float = field(default=0.0, repr=False)

shelf_id: Optional[int] = field(init=False)

chapters: List[str] = field(default_factory=list)

condition: str = field(default="Good", compare=False)

def __post_init__(self):

if self.condition == "Discarded":

self.shelf_id = None

else:

self.shelf_id = 0

In diesem Beispiel haben wir eine __post_init__-Methode erstellt, um shelf_id auf None zu setzen, wenn die condition des Books als "Discarded" initialisiert wird. Dabei verwenden wir field, um shelf_id zu initialisieren und init als False an field zu übergeben. Das hat zur Folge, dass shelf_id nicht in __init__ initialisiert wird.

InitVar

Eine weitere Möglichkeit, die Einrichtung von Python-Datenklassen anzupassen führt über den InitVar-Type. Damit können Sie ein Field angeben, das an __init__ und dann an __post_init__ übergeben wird, aber nicht in der Klasseninstanz gespeichert wird. So können Sie beim Setup einer Datenklasse Parameter aufnehmen, die nur während der Initialisierung verwendet werden. Hier ein Beispiel:

from dataclasses import dataclass, field, InitVar

from typing import List

@dataclass

class Book:

'''Object for tracking physical books in a collection.'''

name: str

condition: InitVar[str] = "Good"

weight: float = field(default=0.0, repr=False)

shelf_id: int = field(init=False)

chapters: List[str] = field(default_factory=list)

def __post_init__(self, condition):

if condition == "Unacceptable":

self.shelf_id = None

else:

Den Type eines Fields auf InitVar zu setzen (wobei der Untertyp der eigentliche Field Type ist) signalisiert @dataclass, dass das Field nicht zu einem Datenklassenfeld werden soll. Stattdessen sollen die Daten als Argument an __post_init__ transferiert werden.

In dieser Version unserer Book-Klasse nutzen wir condition nur im Rahmen der Initialisierungsphase. Wenn wir feststellen, dass condition auf "Unacceptable" gesetzt wurde, setzen wir shelf_id auf None - speichern jedoch condition selbst nicht in der Klasseninstanz.

Python-Datenklassen richtig einsetzen

Python Dataclasses kommen in der Praxis gängigerweise als Substitut für namedtuple zum Einsatz. Im Vergleich bieten Datenklassen dieselben Features und mehr. Sie können einfach unveränderlich gemacht werden, indem @dataclass(frozen=True) als Dekorator zum Einsatz kommt.

Ein anderer möglicher Use Case: Verschachtelte Datenklasseninstanzen könnten Nested Dictionaries ersetzen, die oft diffizil zu handhaben sind. Wenn Sie über eine Library Dataclass mit einer List Property von shelves haben, könnten Sie eine Dataclass ReadingRoom einsetzen, um die Liste zu befüllen. Anschließend ließen sich Methoden hinzufügen, um den Zugriff auf verschachtelte Elemente zu erleichtern.

Aber nicht jede Python-Klasse muss eine Dataclass sein: Wenn Sie eine Klasse nicht in erster Linie als als Daten-Container, sondern hauptsächlich dazu einsetzen, eine Reihe statischer Methoden zu gruppieren, müssen Sie dazu nicht auf eine Dataclass setzen. Ein gängiges Muster bei Parsern ist zum Beispiel eine Klasse, die einen abstrakten Syntaxbaum aufnimmt, diesen durchläuft und je nach Knotentyp Aufrufe an verschiedene Methoden in der Klasse weiterleitet. Da die Parser Class nur über sehr wenige eigene Daten verfügt, ist eine Dataclass an dieser Stelle nicht sinnvoll. (fm)

Dieser Beitrag basiert auf einem Artikel unserer US-Schwesterpublikation Infoworld.