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üssendefault
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 wirdefault
, umweight
auf0.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 sollchapters
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 wirrepr=False
.compare
: Inkludiert standardmäßig (True
) das Field in die automatisch für die Datenklasse generierten Vergleichsmethoden. Weil wir nicht wollen, dasscondition
als Teil des Vergleichs für zwei Bücher verwendet wird, setzen wircompare=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.