None

Python Dataclasses nutzen – so geht’s

Lesen Sie, wie Pyton-Devs von Dataclasses profitieren können. Urbanscape | shutterstock.com



In Python ist alles ein Objekt. Wenn Sie Ihre eigenen, benutzerdefinierten Objekte mit ihren jeweils eigenen Eigenschaften und Methoden kreieren möchten, nutzen Sie dazu das class-Keyword von Python. Python-Klassen auf diese Weise zu erstellen, kann allerdings auch bedeuten, riesige Mengen an Boilerplate-Code schreiben zu müssen. Beispielweise, um die Klasseninstanz der übergebenen Parameter aufzusetzen oder um gängige Funktionen wie Vergleichs-Operatoren zu implementieren.



Die mit Python Version 3.7 eingeführten Dataclasses (rückwirkend auch für Version 3.6 verfügbar gemacht) stellen einen nützlichen und weit weniger aufwendigen Weg dar, Klassen in Python zu erstellen. Mit ihrer Hilfe lassen sich viele Dinge auf wenige, grundlegende Instruktionen reduzieren. In diesem Tutorial lesen Sie, wie Sie Dataclasses in Python optimal einsetzen.



Python Dataclasses erklärt



Werfen Sie einen Blick auf die folgende, konventionelle Python-Klasse:




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})")




Am umständlichsten ist an diesem Beispiel-Code, dass Sie jedes einzelne Argument, das an __init__ übergeben wird, in die Eigenschaften des Objekts kopieren müssen. Wenn Sie es dabei lediglich mit Book zu tun haben, ist der Aufwand überschaubar – aber was, wenn weitere Klassen hinzukommen, etwa Bookshelf, Library, Warehouse und so weiter? Dazu kommt noch: Je mehr Code händisch eingepflegt wird, desto größer die Chance, dass sich Fehler einschleichen.



Und nun dieselbe Klasse – nur 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 innerhalb einer Dataclass Eigenschaften (sogenannte „fields“; Felder) spezifizieren, generiert der @dataclass-Decorator automatisiert sämtlichen Code, der nötig ist, um diese zu initialisieren. Dabei werden auch die Typ-Informationen für jedes Property vorgehalten. Wenn Sie ein Linting-Tool einsetzen, das Typ-Informationen überprüft, stellt das sicher, dass die richtige Art von Variablen an den Klassen-Konstruktor übermittelt wird.



Zudem erstellt @dataclass hinter den Kulissen auch den Code für gängige Dunder-Methoden innerhalb der Klasse. In unserem ersten Beispiel (der konventionellen Python-Klasse) mussten wir unser eigenes __repr__ erstellen. Das übernimmt im Fall einer Dataclass der Decorator für uns. Natürlich ist es bei Bedarf auch möglich, den generierten Code zu überschreiben. Einmal aufgesetzt, ist eine Dataclass auf funktionaler Ebene identisch zu einer regulären Klasse.



Dataclasses-Optionen in Python



Der @dataclass-Decorator bringt diverse eigene (True/False-) Initialisierungsoptionen mit. Diese sind besonders für bestimmte Nischenanwendungsfälle nützlich. Drei Beispiele:




frozen generiert Read-Only-Klasseninstanzen. Wenn Daten einmal zugewiesen sind, kann das nicht mehr geändert werden. Das ist nützlich, wenn Dataclass-Instanzen “hashable” sein sollen – zum Beispiel, um sie als Dictionary Keys zu nutzen. Wenn Sie frozen einsetzen, wird für die generierte Dataclass zudem automatisch eine __hash__-Methode generiert (davon abgesehen können Sie mit unsafe_hash=true auch eine __hash__-Methode für Dataclasses generieren, die nicht read-only sind – allerdings resultiert das in “Unsafe Behavior”).



slots sorgt dafür, dass Dataclass-Instanzen weniger Speicher fressen, indem es ausschließlich fields erlaubt, die innerhalb der Klasse explizit definiert sind. Diese Memory-Einsparungen machen sich allerdings nur in größerem Rahmen bemerkbar – etwa wenn mehrere tausend Instanzen eines bestimmten Objekts generiert werden. Wenn Sie lediglich ein paar Dataclass-Instanzen auf einmal erstellen, lohnt sich der Aufwand sehr wahrscheinlich nicht.  



kw_only definiert sämtliche Felder für die Klassen als “keyword only”. Das ist nützlich, um die Argumente einer Dataclass in Form eines Dictionary zur bereitzustellen.




Dataclass-Felder anpassen



Die Funktionsweise von Dataclasses sollte für die allermeisten Anwendungsfälle ausreichen. Es kann jedoch auch vorkommen, dass Sie feintunen müssen, wie die Felder innerhalb der Dataclass initialisiert werden. Das nachfolgende Code-Beispiel demonstriert, wie Sie dafür die field-Funktion nutzen:




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 eine Instanz von field mit einem Standardwert ausstatten, verändert das die Art und Weise, wie dieses aufgesetzt wird – je nachdem welche Parameter Sie zur Verfügung stellen. Zu den gängigsten Optionen gehören:




default – setzt den Standardwert für das Feld. Verwenden Sie es, wenn Sie weitere Parameter anpassen und zusätzlich einen Standardwert setzen möchten. Im obigen Beispiel kommt default zum Einsatz, um weight auf 0.0 zu setzen.  



default_factory gibt den Namen einer Funktion an, die keine Parameter aufnimmt und ein Objekt zurückgibt, das als Standardwert für das Feld dient. Im obenstehenden Beispiel sollte etwa chapters eine leere Liste darstellen.



repr ist standardmäßig (True) und steuert, ob das betreffende Feld im für die Dataclass automatisch generierten __repr__ auftaucht. In unserem Beispielcode wollten wir nicht, dass das Gewicht des Buches im __repr__ angezeigt wird. Das bewerkstelligt repr=False.



compare ist ebenfalls per Default (True) und fügt das Feld in die automatisch erstellten Vergleichsmethoden der Dataclass ein. In unserem Code-Beispiel wollten wir nicht, dass condition in den Vergleich zweier Bücher einfließt – deswegen setzen wir compare=False.




Python Dataclasses initialisieren



Sie fragen sich eventuell, wie Sie trotz der automatischen Generierung der __init__-Methode für die Dataclass den init-Prozess kontrollieren und feinere Abstimmungen vornehmen können. Dazu stehen zwei Optionen zur Verfügung.



__post_init__



Indem Sie ihre Dataclass-Definition um die __post_init__-Methode ergänzen, können Sie Instruktionen bereitstellen, um Felder oder andere Instanzdaten zu modifizieren:




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 der Zustand des Buchs als "Discarded" initialisiert wird. Dabei nutzen wir field, um shelf_id zu initialisieren und init als False an field zu übergeben. Soll heißen: shelf_id wird nicht in __init__ initialisiert, sondern allgemein als field in der Dataclass registriert – inklusive Typ-Informationen.



InitVar



Der InitVar-Typ bietet einen weiteren Weg, um das Setup von Python Dataclasses anzupassen. Er ermöglicht es, ein Feld zu spezifizieren, das zuerst an __init__ und anschließend an __post_init__ übergeben – aber nicht innerhalb der Klasseninstanz gespeichert wird.



Mit InitVar können Sie Parameter einbeziehen, wenn Sie eine Dataclass aufsetzen, die ausschließlich während der Initalisierung genutzt werden. 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:
self.shelf_id = 0




Den Typ eines Felds auf InitVar zu setzen (dessen Subtyp der eigentliche Field Type ist), signalisiert @dataclass, das Feld nicht zu einem Dataclass-Feld zu machen – aber die Daten als Argument an __post_init__ zu übergeben.



In dieser Version unserer Book-Klasse speichern wir condition nicht als Feld in der Klasseninstanz, sondern nutzen es ausschließlich in der Initialisierungsphase. Wenn sich herausstellen sollte, dass diese "Unacceptable" ist, setzen wir shelf_id auf None. Aber wir speichern condition selbst nicht innerhalb der Klasseninstanz.



Wo Dataclasses sinnvoll sind – und wo nicht



Ein gängiges Anwendungsszenario für Python Dataclasses ist der Ersatz für namedtuple. Dataclasses bieten dieselben oder mehr Features und können ebenso wie namedtuples auch unveränderlich gemacht werden. Dazu wird @dataclass(frozen=True) als Decorator genutzt.



Ein weiterer Use Case: verschachtelte Dictionaries durch verschachtelte Dataclass-Instanzen substitutieren. Angenommen Sie haben die Dataclass Library mit der List-Property shelves. Dann könnten Sie die Dataclass ReadingRoom nutzen, um diese shelves-Liste mit Leben zu füllen. Anschließend können Sie Methoden hinzufügen, die es einfach machen, auf verschachtelte Items zuzugreifen (etwa ein Buch in einem Regal in einem bestimmten Raum).



Zu beachten ist allerdings auch: Nicht jede Python-Klasse muss eine Dataclass sein. Wenn Sie eine Klasse hauptsächlich erstellen, um ein paar statische Methoden zu bündeln, brauchen Sie dafür keine Dataclass. Ein gängiges Muster bei Parsern ist beispielsweise eine Klasse, die einen abstrakten Syntax-Baum entgegennimmt, durchläuft und dabei abhängig vom Node-Typ Aufrufe an verschiedene Methoden innerhalb der Klasse weiterleitet. Weil die Parser-Klasse selbst nur über minimale, eigene Daten verfügt, macht eine Dataclass in diesem Fall keinen Sinn. (fm)