Python dataclasses in Beispielen erklärt

Seit Python Version 3.7 gibt es ein neues Modul für spezielle Daten-Klassen, die dataclasses.

https://docs.python.org/3.7/library/dataclasses.html

@dataclass decorator für eine Klasse benutzen

Mit dem dataclass decorator ergeben sich viele neue Möglichkeiten Daten-Klassen basierend auf ihren Klassenvariablen zu erzeugen. Ein entsprechender decorator markiert eine Klasse als dataclass. Damit wird dafür gesorgt, dass automatisch eine __init__() Methode erzeugt wird, die die Klassenvariablen als Argumente enthält. Sind die Klassenvariablen mit entsprechenden Typen annotiert, erfolgt auch automatisch eine Prüfung in der IDE.

pycharm zeigt eine Warnung wenn der Typ der Variable nicht mit dem definierten Typ übereinstimmt

Ein einfaches Beispiel zeigt, wie eine dataclass benutzt werden kann.

from dataclasses import dataclass


@dataclass
class Car:
    manufacturer: str = None
    model: str = None
    color: str = None
    length: float = None
    seats: int = None
    is_suv: bool = None


def main():
    new_car = Car(
        manufacturer="Tesla",
        model="Model X",
        color="blue",
        length=5.0,
        seats=5,
        is_suv=True,
    )
    print(new_car)
    if new_car.is_suv:
        print(f"{new_car.manufacturer} {new_car.model} is a SUV")

Eine print() Ausgabe der Car dataclass gibt dann auch entsprechend alle Klassenvariablen aus. Der dataclass decorator erstellt m Hintergrund eine __repr__() Methode.

Car(manufacturer='Tesla', model='Model X', color='blue', length=5.0, seats=5, is_suv=True)
Tesla Model X is a SUV

dataclass decorator Parameter

Standardmäßig generiert der dataclass decorator einige Methoden für die dekorierte Klasse. Jedoch nur, wenn diese nicht schon vorhanden sind. Über die Parameter des dataclass decorators kann konkret festgelegt werden, welche Methoden ignoriert oder zur Verfügung stehen sollen.

init Methode

Die __init__() Methode wird standardmäßig immer angelegt. Wie im oben gezeigten Beispiel werden dabei die Klassenvariablen als Parameter benutzt. Sollte die Klasse bereits eine __init__() Methode haben, so wird natürlich diese benutzt und der Parameter des decorators ignoriert.

Setzt man andererseits init=False, so erhält die Klasse keine __init__() Methode, wie folgendes Beispiel zeigt:

@dataclass(init=False)
class CarWithoutInit:
    manufacturer: str = None

def with_init():
    # a class without default init method
    try:
        CarWithoutInit(manufacturer="Tesla")
    except TypeError:
        print("CarWithoutInit could not be created")

    new_car_without_init = CarWithoutInit()
    new_car_without_init.manufacturer = "BMW"
    print(new_car_without_init)

Die Ausgabe auf der console:

CarWithoutInit could not be created
CarWithoutInit(manufacturer='BMW')

repr Methode

Die __repr__() Methode wird ebenfalls standdardmäßig angelegt. Benutzt man die Klasse in einem print() statement oder z.B. in einem f-string, wie im ersten Beispiel oben gezeigt, entsteht ein entsprechender String basierend auf den Klassenvariablen. Auch hier wird der Parameter des decorators ignoriert, wenn es bereits eine __repr__() Methode gibt.

Bei repr=False erfolgt die string Representation der Klasse jedoch im bekannten Schema.

@dataclass(repr=False)
class CarWithoutRepr:
    manufacturer: str = None

def with_repr():
    # a class without default repr method
    new_car_without_repr = CarWithoutRepr(manufacturer="Audi")
    print(new_car_without_repr)

Die Ausgabe auf der console:

<__main__.CarWithoutRepr object at 0x7fe0bec07350e

eq und order Methoden

Die __eq__() Methode, ebenfalls automatisch erzeugt wenn nicht über den decorator Parameter auf False gesetzt, benutzt man um zwei Klassen miteinander zu vergleichen. Klassen gleichen Typs vergleicht Python dann anhand ihrer Klassenvariablen.

def equals():
    # equality comparison
    tesla_a = Car(manufacturer="Tesla")
    tesla_b = Car(manufacturer="Tesla")
    bmw = Car(manufacturer="BMW")
    print("tesla_a == tesla_b :", tesla_a == tesla_b)
    print("tesla_a is tesla_b :", tesla_a is tesla_b)
    print("tesla_a is tesla_a :", tesla_a is tesla_a)
    print("bmw == tesla :", bmw == tesla_a)

Und die entsprechende Ausgabe:

tesla_a == tesla_b : True
tesla_a is tesla_b : False
tesla_a is tesla_a : True
bmw == tesla : False

Setzt man andererseits eq=False , liefert der erste == Vergleich ein anderes Ergebnis.

@dataclass(eq=False)
class CarWithoutEq:
    manufacturer: str = None

def equals():
    # equality comparison disabled
    tesla_a = CarWithoutEq(manufacturer="Tesla")
    tesla_b = CarWithoutEq(manufacturer="Tesla")
    print("tesla_a == tesla_b :", tesla_a == tesla_b)
    print("tesla_a is tesla_b :", tesla_a is tesla_b)
    print("tesla_a is tesla_a :", tesla_a is tesla_a)
tesla_a == tesla_b : False
tesla_a is tesla_b : False
tesla_a is tesla_a : True

Die Methoden __lt__(), __le__(), __gt__(), __ge__() aktiviert man mit dem Parameter order. Anschließend benutzt Python die Klassenvariablen, um einen entsprechenden Vergleich durchzuführen.

@dataclass(order=True)
class CarOrderable:
    seats: int = None
    doors: int = None

def order():
    # order by seats
    car_with_2_seats = CarOrderable(seats=2)
    car_with_3_seats = CarOrderable(seats=3)
    car_with_4_seats = CarOrderable(seats=4)
    print("2 > 3 :", car_with_2_seats > car_with_3_seats)
    print("4 > 3 :", car_with_4_seats > car_with_3_seats)
    print("2 <= 4 :", car_with_2_seats <= car_with_4_seats)

2 > 3 : False
4 > 3 : True
2 <= 4 : True

Sind mehrere Klassenvariablen gesetzt, dann erfolgt der Vergleich in der entsprechenden Reihenfolge der Definition und das erste Ergebnis liefert den Rückgabewert. Daher vergleicht der Python Interpreter in unserem Beispiel auch die doors Variable, um eine Entscheidung zu treffen. Die seats sind identisch und liefern zuerst keine Entscheidung.

car_with_2_seats_and_3_doors = CarOrderable(seats=2, doors=3)
car_with_3_seats_and_2_doors = CarOrderable(seats=3, doors=2)
print(
    "2,3 > 3,2 :",
    car_with_2_seats_and_3_doors >= car_with_3_seats_and_2_doors,
)
print(
    "2,3 < 3,2 :",
    car_with_2_seats_and_3_doors <= car_with_3_seats_and_2_doors,
)

car_with_3_seats_and_3_doors = CarOrderable(seats=3, doors=3)
car_with_3_seats_and_2_doors = CarOrderable(seats=3, doors=2)
print(
    "3,3 > 3,2 :",
    car_with_3_seats_and_3_doors >= car_with_3_seats_and_2_doors,
)
print(
    "3,3 < 3,2 :",
    car_with_3_seats_and_3_doors <= car_with_3_seats_and_2_doors,
)
2,3 > 3,2 : False
2,3 < 3,2 : True
3,3 > 3,2 : True
3,3 < 3,2 : False

frozen Parameter

Setzt man den Parameter frozen auf True, ist die Zuweisung zu Feldern der Klasse nicht mehr möglich. Das ist z.B. bei read-only Instanzen der Fall.

@dataclass(frozen=True)
class ImmutableCar:
    seats: int = None

def frozen():
    immutable_car = ImmutableCar(seats=5)
    print(immutable_car)
    immutable_car.seats = 6
ImmutableCar(seats=5)
Traceback (most recent call last):
  File "/home/christian/PycharmProjects/einfachpython/src/einfachpython/dataclass_examples.py", line 140, in <module>
    frozen()
  File "/home/christian/PycharmProjects/einfachpython/src/einfachpython/dataclass_examples.py", line 131, in frozen
    immutable_car.seatss = 6
  File "<string>", line 4, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'seats'

Warum benutzt man dataclasses?

Der Vorteil von Datenklassen zeigt sich wie so oft in der Vermeidung von unnötigem Code. Durch den decorator ist eine dataclass automatisch mit einer gewissen basis Funktionalität ausgestattet. Instanziieren, Vergleichen und String Ausgaben sind direkt möglich. Daher kann man auf selbstgeschriebene __eq__(), __repr__() oder __init__() Methoden im Normalfall verzichten.

tl;dr

Hier gibt es den Beispielcode in Github: https://github.com/Sprungwunder/einfachpython/blob/master/src/einfachpython/dataclass_examples.py