Datenklassen sind eine der Funktionen von Python, die du, wenn du sie einmal entdeckt hast, nie wieder verwenden wirst. Betrachte diese reguläre Klasse:
classExercise:def__init__(self, name, reps, sets, weight):
self.name = name
self.reps = reps
self.sets = sets
self.weight = weight
Bereitgestellt von
Meiner Meinung nach ist diese Klassendefinition sehr ineffizient - in der Methode __init__ wiederholst du jeden Parameter mindestens dreimal. Das klingt vielleicht nicht nach einer großen Sache, aber überlege mal, wie oft du in deinem Leben Klassen mit viel mehr Parametern schreibst.
Zum Vergleich: Schau dir die Datenklassen-Alternative des obigen Codes an:
from dataclasses import dataclass
@dataclassclassExercise:
name:str
reps:int
sets:int
weight:float# Weight in lbsBereitgestellt von
Dieses bescheiden aussehende Stück Code ist um Größenordnungen besser als eine normale Klasse. Der winzige Dekorator @dataclass implementiert die Klassen __init__, __repr__, __eq__ hinter den Kulissen, was manuell mindestens 20 Zeilen Code erfordert hätte.
Außerdem sind viele andere Funktionen wie Vergleichsoperatoren, Objektordnung und Unveränderlichkeit nur eine einzige Zeile davon entfernt, für unsere Klasse erstellt zu werden.
In diesem Tutorial wollen wir dir zeigen, warum Datenklassen das Beste sind, was Python passieren konnte, wenn du objektorientierte Programmierung liebst.
Lass uns loslegen!
Grundlagen der Python-Datenklassen
Im Folgenden werden einige der grundlegenden Konzepte von Python-Datenklassen erläutert, die Python so nützlich machen.
Einige Methoden werden automatisch in Datenklassen generiert
Trotz all ihrer Funktionen sind Datenklassen ganz normale Klassen, die viel weniger Code benötigen, um die gleiche Funktionalität zu implementieren. Hier ist wieder die Exercise Klasse:
from dataclasses import dataclass
@dataclassclassExercise:
name:str
reps:int
sets:int
weight:float
ex1 = Exercise("Bench press",10,3,52.5)# Verifying Exercise is a regular class
ex1.name
'Bench press'Bereitgestellt von
Im Moment hat Exercise bereits die Methoden __repr__ und __eq__ implementiert. Lass es uns überprüfen:
repr(ex1)"Exercise(name='Bench press', reps=10, sets=3, weight=52.5)"Bereitgestellt von
Die Objektrepräsentation eines Objekts repr muss den Code zurückgeben, der sich selbst wiederherstellen kann, und wir können sehen, dass genau das bei ex1 der Fall ist.
Im Vergleich dazu würde Exercise auf die alte Art und Weise wie folgt aussehen:
classExercise:def__init__(self, name, reps, sets, weight):
self.name = name
self.reps = reps
self.sets = sets
self.weight = weight
ex3 = Exercise("Bench press",10,3,52.5)
ex3
<__main__.Exercise at 0x7f6834100130>Bereitgestellt von
Sieht ziemlich furchtbar und nutzlos aus!
Überprüfen wir nun die Existenz von __eq__, dem Gleichheitsoperator:
# Redefine the class@dataclassclassExercise:
name:str
reps:int
sets:int
weight:float
ex1 = Exercise("Bench press",10,3,52.5)
ex2 = Exercise("Bench press",10,3,52.5)Bereitgestellt von
Der Vergleich der Klasse mit sich selbst und mit einer anderen Klasse mit identischen Parametern muss True ergeben:
Und das tut sie auch! Im normalen Unterricht wäre diese Logik mühsam zu schreiben gewesen.
Datenklassen erfordern Typ-Hinweise
Wie du vielleicht schon bemerkt hast, benötigen Datenklassen bei der Definition von Feldern Typ-Hinweise. In der Tat erlauben Datenklassen jeden Typ aus dem typing Modul. So kannst du zum Beispiel ein Feld erstellen, das den Datentyp Any akzeptiert:
from typing import Any
@dataclassclassDummy:
attr: AnyBereitgestellt von
Die Besonderheit von Python ist jedoch, dass die Typen nicht erzwungen werden, obwohl die Datenklassen Hinweise auf den Typ benötigen.
Zum Beispiel kann die Erstellung einer Instanz der Klasse Exercise mit völlig falschen Datentypen ohne Fehler ausgeführt werden:
silly_exercise = Exercise("Bench press","ten","three sets",52.5)
silly_exercise.sets
“Three sets”Bereitgestellt von
Wenn du Datentypen erzwingen willst, musst du Typprüfungen wie Mypy verwenden.
Datenklassen erlauben Standardwerte in Feldern
Bis jetzt haben wir unseren Klassen noch keine Vorgaben hinzugefügt. Das müssen wir ändern:
@dataclassclassExercise:
name:str="Push-ups"
reps:int=10
sets:int=3
weight:float=0# Now, all fields have defaults
ex5 = Exercise()
ex5
Exercise(name='Push-ups', reps=10, sets=3, weight=0)Bereitgestellt von
Denke daran, dass Nicht-Standardfelder nicht auf Standardfelder folgen können. Der folgende Code zum Beispiel wird einen Fehler auslösen:
@dataclassclassExercise:
name:str="Push-ups"
reps:int=10
sets:int=3
weight:float# NOT ALLOWED
ex5 = Exercise()
ex5
TypeError: non-default argument 'weight' follows default argumentBereitgestellt von
In der Praxis wirst du nur selten Standardwerte mit der name: type = value Syntax definieren.
Stattdessen verwendest du die Funktion field, die mehr Kontrolle über die einzelnen Felddefinitionen ermöglicht:
from dataclasses import field
@dataclassclassExercise:
name:str= field(default="Push-up")
reps:int= field(default=10)
sets:int= field(default=3)
weight:float= field(default=0)# Now, all fields have defaults
ex5 = Exercise()
ex5
Exercise(name='Push-up', reps=10, sets=3, weight=0)Bereitgestellt von
Die Funktion field hat mehr Parameter, wie zum Beispiel:
repr
init
compare
default_factory
und so weiter. Wir werden diese in den nächsten Abschnitten besprechen.
Datenklassen können mit einer Funktion erstellt werden
Ein letzter Hinweis zu den Grundlagen der Datenklassen ist, dass ihre Definition noch kürzer sein kann, wenn du die Funktion make_dataclass verwendest:
from dataclasses import make_dataclass
Exercise = make_dataclass("Exercise",[("name",str),("reps",int),("sets",int),("weight",float),],)
ex3 = Exercise("Deadlifts",8,3,69.0)
ex3
Exercise(name='Deadlifts', reps=8, sets=3, weight=69.0)Bereitgestellt von
Aber du verlierst dabei die Lesbarkeit, deshalb empfehle ich diese Funktion nicht.
Fortgeschrittene Python-Datenklassen
In diesem Abschnitt werden wir erweiterte Funktionen von Datenklassen besprechen, die mehr Vorteile bringen. Eine solche Funktion ist die Standardfabrik.
Standard-Fabriken
Um die Standardfabriken zu erklären, erstellen wir eine weitere Klasse namens WorkoutSession, die zwei Felder akzeptiert:
from dataclasses import dataclass
from typing import List
@dataclassclassExercise:
name:str="Push-ups"
reps:int=10
sets:int=3
weight:float=0@dataclassclassWorkoutSession:
exercises: List[Exercise]
duration_minutes:intBereitgestellt von
Indem wir den Typ List verwenden, geben wir an, dass WorkoutSession eine Liste von Exercise Instanzen annimmt.
# Define the Exercise instances for HIIT training
ex1 = Exercise(name="Burpees", reps=15, sets=3)
ex2 = Exercise(name="Mountain Climbers", reps=20, sets=3)
ex3 = Exercise(name="Jump Squats", reps=12, sets=3)
exercises_monday =[ex1, ex2, ex3]
hiit_monday = WorkoutSession(exercises=exercises_monday, duration_minutes=30)Bereitgestellt von
Momentan müssen für jede Trainingseinheit Übungen initialisiert werden. Aber das spiegelt nicht wider, wie die Menschen trainieren - zuerst beginnen sie eine Sitzung (wahrscheinlich in einer App) und fügen dann während des Trainings Übungen hinzu.
Wir müssen also in der Lage sein, Sitzungen ohne Übungen und ohne Dauer zu erstellen. Um dies zu erreichen, fügen wir eine leere Liste als Standardwert für exercises hinzu:
@dataclassclassWorkoutSession:
exercises: List[Exercise]=[]
duration_minutes:int=None
hiit_monday = WorkoutSession("25-02-2024")
ValueError: mutable default <class'list'>for field exercises isnot allowed: use default_factoryBereitgestellt von
Wir erhielten jedoch einen Fehler - es stellte sich heraus, dass Datenklassen keine veränderbaren Standardwerte zulassen.
Zum Glück können wir das beheben, indem wir eine Standardfabrik verwenden:
Der Parameter default_factory akzeptiert eine Funktion, die einen Anfangswert für ein Datenklassenfeld zurückgibt. Das bedeutet, dass sie jede beliebige Funktion annehmen kann:
Dies gilt unabhängig davon, ob das Ergebnis der Funktion veränderbar ist oder nicht.
Wenn wir darüber nachdenken, beginnen die meisten Menschen ihr Training mit Aufwärmübungen, die für jede Art von Training typisch sind. Es kann also sein, dass der Beginn einer Sitzung ohne Übungen nicht das ist, was manche Leute wollen.
Stattdessen wollen wir eine Funktion erstellen, die drei Aufwärmprogramme zurückgibt Exercises:
defcreate_warmup():return[
Exercise("Jumping jacks",30,1),
Exercise("Squat lunges",10,2),
Exercise("High jumps",20,1),]@dataclassclassWorkoutSession:
exercises: List[Exercise]= field(default_factory=create_warmup)
duration_minutes:int=5# Increase the default duration as well
hiit_monday = WorkoutSession()
hiit_monday
WorkoutSession(exercises=[Exercise(name='Jumping jacks', reps=30, sets=1, weight=0), Exercise(name='Squat lunges', reps=10, sets=2, weight=0), Exercise(name='High jumps', reps=20, sets=1, weight=0)], duration_minutes=5)Bereitgestellt von
Wenn wir jetzt eine Unterrichtseinheit erstellen, werden bereits einige Aufwärmübungen aufgezeichnet. Die neue Version von WorkoutSession hat eine Standarddauer von fünf Minuten, um dies zu berücksichtigen.
Hinzufügen von Methoden zu Datenklassen
Da Datenklassen reguläre Klassen sind, bleibt das Hinzufügen von Methoden zu ihnen dasselbe. Fügen wir zwei Methoden zu unserer WorkoutSession Datenklasse hinzu:
Mit diesen Methoden können wir jetzt jede neue Aktivität in einer Sitzung protokollieren:
hiit_monday = WorkoutSession()# Log a new exercise
new_exercise = Exercise("Deadlifts",6,4,60)
hiit_monday.add_exercise(new_exercise)
hiit_monday.increase_duration(15)Bereitgestellt von
Wenn wir die Sitzung ausdrucken, ist ihre Standarddarstellung zu ausführlich und unleserlich, da sie den Code zur Neuerstellung des Objekts enthält. Das müssen wir ändern.
__repr__ und __str__ in Datenklassen
Datenklassen implementieren __repr__ automatisch, aber nicht __str__. So kann die Klasse auf __repr__ zurückgreifen, wenn wir sie zum Drucken aufrufen.
Also setzen wir dieses Verhalten außer Kraft, indem wir __str__ selbst definieren:
@dataclassclassExercise:
name:str="Push-ups"
reps:int=10
sets:int=3
weight:float=0def__str__(self):
base =f"{self.name}: {self.reps}/{self.sets}"if self.weight ==0:return base
return base +f", {self.weight} lbs"
ex1 = Exercise(name="Burpees", reps=15, sets=3)
ex1
Exercise(name='Burpees', reps=15, sets=3, weight=0)Bereitgestellt von
Die __repr__ ist immer noch die gleiche, aber wenn wir print aufrufen:
Die Frühlingsdarstellung der Klasse ist viel schöner. Und jetzt lass uns auch WorkoutSession reparieren:
@dataclassclassWorkoutSession:
exercises: List[Exercise]= field(default_factory=create_warmup)
duration_minutes:int=5# Increase the default duration as welldefadd_exercise(self, exercise: Exercise):
self.exercises.append(exercise)defincrease_duration(self, minutes:int):
self.duration_minutes += minutes
def__str__(self):
base =""for ex in self.exercises:
base +=str(ex)+"\n"
base +=f"\nSession duration: {self.duration_minutes} minutes."return base
hiit_monday = WorkoutSession()print(hiit_monday)
Jumping jacks:30/1
Squat lunges:10/2
High jumps:20/1
Session duration:5 minutes.Bereitgestellt von
Hinweis: Verwende die Schaltfläche "Code erklären" am Ende des Snippets, um eine zeilenweise Erklärung des Codes zu erhalten.
Jetzt haben wir eine lesbare und kompakte Ausgabe.
Vergleich in Datenklassen
Bei vielen Klassen ist es sinnvoll, ihre Objekte nach einer bestimmten Logik zu vergleichen. Beim Training kann es die Trainingsdauer, die Trainingsintensität oder das Gewicht sein.
Schauen wir uns zunächst an, was passiert, wenn wir versuchen, zwei Workouts im aktuellen Zustand zu vergleichen:
hiit_wednesday = WorkoutSession()
hiit_wednesday.add_exercise(Exercise("Pull-ups",7,3))print(hiit_wednesday)
Jumping jacks:30/1
Squat lunges:10/2
High jumps:20/1
Pull-ups:7/3
Session duration:5 minutes.
hiit_monday > hiit_wednesday
TypeError:'>'not supported between instances of 'WorkoutSession'and'WorkoutSession'Bereitgestellt von
Wir erhalten eine TypeError, da Datenklassen keine Vergleichsoperatoren implementieren. Das lässt sich aber leicht beheben, indem du den Parameter order auf True setzt:
Dieses Mal funktioniert der Vergleich, aber was vergleichen wir überhaupt?
In Datenklassen wird der Vergleich in der Reihenfolge durchgeführt, in der die Felder definiert sind. Im Moment werden die Klassen anhand der Trainingsdauer verglichen, da das erste Feld, exercises, nicht standardisierte Objekte enthält.
Wir können das überprüfen, indem wir die Dauer der Mittwochssitzung verlängern:
Auch wenn die Montagssitzung länger dauert, zeigt der Vergleich, dass sie kleiner ist als die Mittwochssitzung. Der Grund dafür ist, dass "25" beim Python-Stringvergleich vor "27" kommt.
Wie können wir also die Reihenfolge der Felder beibehalten und die Sitzungen trotzdem nach der Trainingsdauer sortieren? Das geht ganz einfach über die Funktion field:
Indem wir compare für ein beliebiges Feld auf False setzen, schließen wir es von der Sortierung aus, wie das obige Ergebnis zeigt.
Manipulation des Post-Init-Feldes
Im Moment haben wir eine Standarddauer von fünf Minuten, um Aufwärmübungen zu berücksichtigen. Das macht aber nur Sinn, wenn ein Nutzer eine Sitzung mit einem Warm-up beginnt. Was ist, wenn sie eine Sitzung mit anderen Übungen beginnen?
new_session = WorkoutSession([Exercise("Diamond push-ups",10,3)])
new_session.duration_minutes
5Bereitgestellt von
Für nur eine einzige Übung beträgt die Gesamtdauer fünf Minuten, was unlogisch ist. Jede Einheit muss ihre Dauer dynamisch anhand der Anzahl der Sätze jeder Übung schätzen. Das bedeutet, dass wir duration_minutes von dem Feld exercises abhängig machen sollten.
Lass es uns umsetzen:
@dataclassclassWorkoutSession:
exercises: List[Exercise]= field(default_factory=create_warmup)
duration_minutes:int= field(default=0, init=False)def__post_init__(self):
set_duration =3for ex in self.exercises:
self.duration_minutes += ex.sets * set_duration
...Bereitgestellt von
Dieses Mal definieren wir duration_minutes, wobei init auf False gesetzt ist, um die Initialisierung des Feldes zu verzögern.
Dann aktualisieren wir in einer speziellen Methode __post_init__ den Wert auf der Grundlage der Gesamtzahl der Sätze in jedem Exercise.
Wenn wir nun WorkoutSession initialisieren, wird duration_minutes dynamisch um drei Minuten für jeden Satz in jeder Übung erhöht.
# Adding an exercise with three sets
hiit_friday = WorkoutSession([Exercise("Diamond push-ups",10,3)])
hiit_friday.duration_minutes
9Bereitgestellt von
Wenn du ein Feld definieren willst, das von anderen Feldern deiner Datenklasse abhängt, kannst du die __post_init__ Logik verwenden.
Unveränderlichkeit in Datenklassen
Unsere WorkoutSession Datenklasse ist fast fertig; sie muss nur noch geschützt werden. Im Moment kann es ziemlich leicht durcheinander gebracht werden:
hiit_friday.duration_minutes =1000
hiit_friday.duration_minutes
1000del hiit_friday.exercisesBereitgestellt von
Wir wollen alle Felder unserer Klassen schützen, damit sie nur in der von uns gewünschten Weise geändert werden können. Um dies zu erreichen, bietet der @dataclass Dekorator ein praktisches frozen Argument:
@dataclass(frozen=True)classFrozenExercise:
name:str
reps:int
sets:int
weight:int|float=0
ex1 = FrozenExercise("Muscle-ups",5,3)Bereitgestellt von
Wenn wir jetzt ein Feld ändern wollen, erhalten wir eine Fehlermeldung:
ex1.sets =5
FrozenInstanceError: cannot assign to field 'sets'Bereitgestellt von
Wenn du frozen auf True setzt, werden automatisch die Methoden __deleteattr__ und __setattr__ für jedes Feld hinzugefügt, damit sie nach der Initialisierung vor dem Löschen oder Aktualisieren geschützt sind. Außerdem können andere keine neuen Felder mehr hinzufügen:
ex1.new_field =10
FrozenInstanceError: cannot assign to field 'new_field'Bereitgestellt von
Diese Funktionalität würde Dutzende von Codezeilen umfassen, wenn wir es mit traditionellen Klassen zu tun hätten.
Beachte aber bitte, dass wir unsere Klassen nicht wirklich unveränderlich machen können. Schreiben wir zum Beispiel die WorkoutSession um, indem wir frozen auf True setzen:
@dataclass(frozen=True)classImmutableWorkoutSession:
exercises: List[Exercise]= field(default_factory=create_warmup)
duration_minutes:int=5
session1 = ImmutableWorkoutSession()Bereitgestellt von
Wie erwartet, können wir die Liste der Übungen nicht direkt ändern:
exercises ist jedoch eine Liste, die vollständig veränderbar ist, sodass die folgende Operation möglich ist:
# Ändern eines der Elemente in einer Liste
# Changing one of the elements in a list
session1.exercises[1]= FrozenExercise("Totally new exercise",5,5)print(session1)
ImmutableWorkoutSession(exercises=[Exercise(name='Jumping jacks', reps=30, sets=1, weight=0), FrozenExercise(name='Totally new exercise', reps=5, sets=5, weight=0), Exercise(name='High jumps', reps=20, sets=1, weight=0)], duration_minutes=5)Bereitgestellt von
Zum Schutz vor versehentlichen Änderungen wird daher empfohlen, unveränderliche Objekte wie Tupel für Feldwerte zu verwenden.
Vererbung in Datenklassen
Ein letzter Punkt, den wir behandeln werden, ist die Reihenfolge der Felder in Eltern- und Kindklassen.
Da Datenklassen reguläre Klassen sind, funktioniert die Vererbung wie üblich:
@dataclass(frozen=True)classImmutableWorkoutSession:
exercises: List[Exercise]= field(default_factory=create_warmup)
duration_minutes:int=5@dataclass(frozen=True)classCardioWorkoutSession(ImmutableWorkoutSession):passBereitgestellt von
Da aber das letzte Feld in der übergeordneten Klasse (ImmutableWorkoutSession) einen Standardwert hat, müssen alle Felder in den untergeordneten Klassen Standardwerte haben.
Dies ist zum Beispiel nicht erlaubt:
@dataclass(frozen=True)classImmutableWorkoutSession:
exercises: List[Exercise]= field(default_factory=create_warmup)
duration_minutes:int=5@dataclass(frozen=True)classCardioWorkoutSession(ImmutableWorkoutSession):
intensity_level:str# Not allowed, must have a default
TypeError: non-default argument 'intensity_level' follows default argument
Bereitgestellt von
Nachteile von Datenklassen und weitere Ressourcen
Die Datenklassen wurden seit Python 3.7 stetig verbessert (sie waren von Anfang an großartig) und decken viele Anwendungsfälle ab, in denen du vielleicht Klassen schreiben musst. In den folgenden Szenarien können sie jedoch nachteilig sein:
Benutzerdefinierte __init__ Methoden
Benutzerdefinierte __new__ Methoden
Verschiedene Vererbungsmuster
Und viele mehr, wie in diesem tollen Reddit-Thread diskutiert. Wenn du eine detailliertere Begründung dafür suchst, warum Datenklassen eingeführt wurden und warum sie kein einfacher Ersatz für normale Klassendefinitionen sind, lies PEP 557.
Wenn du dich für objektorientierte Programmierung im Allgemeinen interessierst, ist dies ein Kurs, mit dem du deine Reise fortsetzen kannst:
Im Grunde genommen sind Datenklassen ausgefeiltere Strukturen, um Daten effizienter zu speichern und abzurufen. Python verfügt jedoch über viele andere Datenstrukturen, die diese Aufgabe mehr oder weniger auf ähnliche Weise erfüllen. Im letzten Kapitel des Kurses "Datentypen für die Datenwissenschaft" erfährst du zum Beispiel mehr über Zähler, defaultdicts und namedtuples.
Mach dich mit Python-Datenstrukturen vertraut: Lerne mehr über Datentypen und primitive sowie nicht-primitive Datenstrukturen wie Strings, Listen, Stapel usw.
Lerne mehr über abstrakte Klassen in Python, wozu sie gut sind und wie du mit dem Modul „abc“ einheitliche Schnittstellen sicherstellen kannst. Enthält praktische Beispiele und bewährte Methoden für eine effektive Umsetzung.
Erforsche Pythons match-case: eine Anleitung zu seiner Syntax, Anwendungen in Data Science und ML sowie eine vergleichende Analyse mit dem traditionellen switch-case.
Erweitere deine Datenkenntnisse, entdecke, wie du Daten manipulieren und visualisieren kannst, und wende fortgeschrittene Analysen an, um datengestützte Entscheidungen zu treffen.
Datenklassen sind eine der Funktionen von Python, die du, wenn du sie einmal entdeckt hast, nie wieder verwenden wirst. Betrachte diese reguläre Klasse:
Meiner Meinung nach ist diese Klassendefinition sehr ineffizient - in der Methode
__init__wiederholst du jeden Parameter mindestens dreimal. Das klingt vielleicht nicht nach einer großen Sache, aber überlege mal, wie oft du in deinem Leben Klassen mit viel mehr Parametern schreibst.Zum Vergleich: Schau dir die Datenklassen-Alternative des obigen Codes an:
Dieses bescheiden aussehende Stück Code ist um Größenordnungen besser als eine normale Klasse. Der winzige Dekorator
@dataclassimplementiert die Klassen__init__,__repr__,__eq__hinter den Kulissen, was manuell mindestens 20 Zeilen Code erfordert hätte.Außerdem sind viele andere Funktionen wie Vergleichsoperatoren, Objektordnung und Unveränderlichkeit nur eine einzige Zeile davon entfernt, für unsere Klasse erstellt zu werden.
In diesem Tutorial wollen wir dir zeigen, warum Datenklassen das Beste sind, was Python passieren konnte, wenn du objektorientierte Programmierung liebst.
Lass uns loslegen!
Grundlagen der Python-Datenklassen
Im Folgenden werden einige der grundlegenden Konzepte von Python-Datenklassen erläutert, die Python so nützlich machen.
Einige Methoden werden automatisch in Datenklassen generiert
Trotz all ihrer Funktionen sind Datenklassen ganz normale Klassen, die viel weniger Code benötigen, um die gleiche Funktionalität zu implementieren. Hier ist wieder die
ExerciseKlasse:Im Moment hat
Exercisebereits die Methoden__repr__und__eq__implementiert. Lass es uns überprüfen:Die Objektrepräsentation eines Objekts
reprmuss den Code zurückgeben, der sich selbst wiederherstellen kann, und wir können sehen, dass genau das beiex1der Fall ist.Im Vergleich dazu würde
Exerciseauf die alte Art und Weise wie folgt aussehen:Sieht ziemlich furchtbar und nutzlos aus!
Überprüfen wir nun die Existenz von
__eq__, dem Gleichheitsoperator:Der Vergleich der Klasse mit sich selbst und mit einer anderen Klasse mit identischen Parametern muss True ergeben:
Und das tut sie auch! Im normalen Unterricht wäre diese Logik mühsam zu schreiben gewesen.
Datenklassen erfordern Typ-Hinweise
Wie du vielleicht schon bemerkt hast, benötigen Datenklassen bei der Definition von Feldern Typ-Hinweise. In der Tat erlauben Datenklassen jeden Typ aus dem
typingModul. So kannst du zum Beispiel ein Feld erstellen, das den DatentypAnyakzeptiert:Die Besonderheit von Python ist jedoch, dass die Typen nicht erzwungen werden, obwohl die Datenklassen Hinweise auf den Typ benötigen.
Zum Beispiel kann die Erstellung einer Instanz der Klasse
Exercisemit völlig falschen Datentypen ohne Fehler ausgeführt werden:Wenn du Datentypen erzwingen willst, musst du Typprüfungen wie Mypy verwenden.
Datenklassen erlauben Standardwerte in Feldern
Bis jetzt haben wir unseren Klassen noch keine Vorgaben hinzugefügt. Das müssen wir ändern:
Denke daran, dass Nicht-Standardfelder nicht auf Standardfelder folgen können. Der folgende Code zum Beispiel wird einen Fehler auslösen:
In der Praxis wirst du nur selten Standardwerte mit der
name: type = valueSyntax definieren.Stattdessen verwendest du die Funktion
field, die mehr Kontrolle über die einzelnen Felddefinitionen ermöglicht:Die Funktion
fieldhat mehr Parameter, wie zum Beispiel:reprinitcomparedefault_factoryund so weiter. Wir werden diese in den nächsten Abschnitten besprechen.
Datenklassen können mit einer Funktion erstellt werden
Ein letzter Hinweis zu den Grundlagen der Datenklassen ist, dass ihre Definition noch kürzer sein kann, wenn du die Funktion
make_dataclassverwendest:Aber du verlierst dabei die Lesbarkeit, deshalb empfehle ich diese Funktion nicht.
Fortgeschrittene Python-Datenklassen
In diesem Abschnitt werden wir erweiterte Funktionen von Datenklassen besprechen, die mehr Vorteile bringen. Eine solche Funktion ist die Standardfabrik.
Standard-Fabriken
Um die Standardfabriken zu erklären, erstellen wir eine weitere Klasse namens
WorkoutSession, die zwei Felder akzeptiert:Indem wir den Typ
Listverwenden, geben wir an, dassWorkoutSessioneine Liste vonExerciseInstanzen annimmt.Momentan müssen für jede Trainingseinheit Übungen initialisiert werden. Aber das spiegelt nicht wider, wie die Menschen trainieren - zuerst beginnen sie eine Sitzung (wahrscheinlich in einer App) und fügen dann während des Trainings Übungen hinzu.
Wir müssen also in der Lage sein, Sitzungen ohne Übungen und ohne Dauer zu erstellen. Um dies zu erreichen, fügen wir eine leere Liste als Standardwert für
exerciseshinzu:Wir erhielten jedoch einen Fehler - es stellte sich heraus, dass Datenklassen keine veränderbaren Standardwerte zulassen.
Zum Glück können wir das beheben, indem wir eine Standardfabrik verwenden:
Der Parameter
default_factoryakzeptiert eine Funktion, die einen Anfangswert für ein Datenklassenfeld zurückgibt. Das bedeutet, dass sie jede beliebige Funktion annehmen kann:tupledictsetDies gilt unabhängig davon, ob das Ergebnis der Funktion veränderbar ist oder nicht.
Wenn wir darüber nachdenken, beginnen die meisten Menschen ihr Training mit Aufwärmübungen, die für jede Art von Training typisch sind. Es kann also sein, dass der Beginn einer Sitzung ohne Übungen nicht das ist, was manche Leute wollen.
Stattdessen wollen wir eine Funktion erstellen, die drei Aufwärmprogramme zurückgibt
Exercises:Wenn wir jetzt eine Unterrichtseinheit erstellen, werden bereits einige Aufwärmübungen aufgezeichnet. Die neue Version von
WorkoutSessionhat eine Standarddauer von fünf Minuten, um dies zu berücksichtigen.Hinzufügen von Methoden zu Datenklassen
Da Datenklassen reguläre Klassen sind, bleibt das Hinzufügen von Methoden zu ihnen dasselbe. Fügen wir zwei Methoden zu unserer
WorkoutSessionDatenklasse hinzu:Mit diesen Methoden können wir jetzt jede neue Aktivität in einer Sitzung protokollieren:
Aber es gibt ein Problem:
Wenn wir die Sitzung ausdrucken, ist ihre Standarddarstellung zu ausführlich und unleserlich, da sie den Code zur Neuerstellung des Objekts enthält. Das müssen wir ändern.
__repr__und__str__in DatenklassenDatenklassen implementieren
__repr__automatisch, aber nicht__str__. So kann die Klasse auf__repr__zurückgreifen, wenn wir sie zum Drucken aufrufen.Also setzen wir dieses Verhalten außer Kraft, indem wir
__str__selbst definieren:Die
__repr__ist immer noch die gleiche, aber wenn wirprintaufrufen:Die Frühlingsdarstellung der Klasse ist viel schöner. Und jetzt lass uns auch
WorkoutSessionreparieren:Hinweis: Verwende die Schaltfläche "Code erklären" am Ende des Snippets, um eine zeilenweise Erklärung des Codes zu erhalten.
Jetzt haben wir eine lesbare und kompakte Ausgabe.
Vergleich in Datenklassen
Bei vielen Klassen ist es sinnvoll, ihre Objekte nach einer bestimmten Logik zu vergleichen. Beim Training kann es die Trainingsdauer, die Trainingsintensität oder das Gewicht sein.
Schauen wir uns zunächst an, was passiert, wenn wir versuchen, zwei Workouts im aktuellen Zustand zu vergleichen:
Wir erhalten eine
TypeError, da Datenklassen keine Vergleichsoperatoren implementieren. Das lässt sich aber leicht beheben, indem du den ParameterorderaufTruesetzt:Dieses Mal funktioniert der Vergleich, aber was vergleichen wir überhaupt?
In Datenklassen wird der Vergleich in der Reihenfolge durchgeführt, in der die Felder definiert sind. Im Moment werden die Klassen anhand der Trainingsdauer verglichen, da das erste Feld,
exercises, nicht standardisierte Objekte enthält.Wir können das überprüfen, indem wir die Dauer der Mittwochssitzung verlängern:
Wie erwartet, haben wir
Falseerhalten.Was würde aber passieren, wenn das erste Feld von
Workouteine andere Art von Feld wäre, zum Beispiel ein String? Lass es uns herausfinden:Auch wenn die Montagssitzung länger dauert, zeigt der Vergleich, dass sie kleiner ist als die Mittwochssitzung. Der Grund dafür ist, dass "25" beim Python-Stringvergleich vor "27" kommt.
Wie können wir also die Reihenfolge der Felder beibehalten und die Sitzungen trotzdem nach der Trainingsdauer sortieren? Das geht ganz einfach über die Funktion
field:Indem wir
comparefür ein beliebiges Feld aufFalsesetzen, schließen wir es von der Sortierung aus, wie das obige Ergebnis zeigt.Manipulation des Post-Init-Feldes
Im Moment haben wir eine Standarddauer von fünf Minuten, um Aufwärmübungen zu berücksichtigen. Das macht aber nur Sinn, wenn ein Nutzer eine Sitzung mit einem Warm-up beginnt. Was ist, wenn sie eine Sitzung mit anderen Übungen beginnen?
Für nur eine einzige Übung beträgt die Gesamtdauer fünf Minuten, was unlogisch ist. Jede Einheit muss ihre Dauer dynamisch anhand der Anzahl der Sätze jeder Übung schätzen. Das bedeutet, dass wir
duration_minutesvon dem Feldexercisesabhängig machen sollten.Lass es uns umsetzen:
Dieses Mal definieren wir
duration_minutes, wobeiinitaufFalsegesetzt ist, um die Initialisierung des Feldes zu verzögern.Dann aktualisieren wir in einer speziellen Methode
__post_init__den Wert auf der Grundlage der Gesamtzahl der Sätze in jedemExercise.Wenn wir nun
WorkoutSessioninitialisieren, wirdduration_minutesdynamisch um drei Minuten für jeden Satz in jeder Übung erhöht.Wenn du ein Feld definieren willst, das von anderen Feldern deiner Datenklasse abhängt, kannst du die
__post_init__Logik verwenden.Unveränderlichkeit in Datenklassen
Unsere
WorkoutSessionDatenklasse ist fast fertig; sie muss nur noch geschützt werden. Im Moment kann es ziemlich leicht durcheinander gebracht werden:Wir wollen alle Felder unserer Klassen schützen, damit sie nur in der von uns gewünschten Weise geändert werden können. Um dies zu erreichen, bietet der
@dataclassDekorator ein praktischesfrozenArgument:Wenn wir jetzt ein Feld ändern wollen, erhalten wir eine Fehlermeldung:
Wenn du
frozenaufTruesetzt, werden automatisch die Methoden__deleteattr__und__setattr__für jedes Feld hinzugefügt, damit sie nach der Initialisierung vor dem Löschen oder Aktualisieren geschützt sind. Außerdem können andere keine neuen Felder mehr hinzufügen:Diese Funktionalität würde Dutzende von Codezeilen umfassen, wenn wir es mit traditionellen Klassen zu tun hätten.
Beachte aber bitte, dass wir unsere Klassen nicht wirklich unveränderlich machen können. Schreiben wir zum Beispiel die
WorkoutSessionum, indem wirfrozenaufTruesetzen:Wie erwartet, können wir die Liste der Übungen nicht direkt ändern:
exercisesist jedoch eine Liste, die vollständig veränderbar ist, sodass die folgende Operation möglich ist:# Ändern eines der Elemente in einer Liste
Zum Schutz vor versehentlichen Änderungen wird daher empfohlen, unveränderliche Objekte wie Tupel für Feldwerte zu verwenden.
Vererbung in Datenklassen
Ein letzter Punkt, den wir behandeln werden, ist die Reihenfolge der Felder in Eltern- und Kindklassen.
Da Datenklassen reguläre Klassen sind, funktioniert die Vererbung wie üblich:
Da aber das letzte Feld in der übergeordneten Klasse (
ImmutableWorkoutSession) einen Standardwert hat, müssen alle Felder in den untergeordneten Klassen Standardwerte haben.Dies ist zum Beispiel nicht erlaubt:
Nachteile von Datenklassen und weitere Ressourcen
Die Datenklassen wurden seit Python 3.7 stetig verbessert (sie waren von Anfang an großartig) und decken viele Anwendungsfälle ab, in denen du vielleicht Klassen schreiben musst. In den folgenden Szenarien können sie jedoch nachteilig sein:
__init__Methoden__new__MethodenUnd viele mehr, wie in diesem tollen Reddit-Thread diskutiert. Wenn du eine detailliertere Begründung dafür suchst, warum Datenklassen eingeführt wurden und warum sie kein einfacher Ersatz für normale Klassendefinitionen sind, lies PEP 557.
Wenn du dich für objektorientierte Programmierung im Allgemeinen interessierst, ist dies ein Kurs, mit dem du deine Reise fortsetzen kannst:
Im Grunde genommen sind Datenklassen ausgefeiltere Strukturen, um Daten effizienter zu speichern und abzurufen. Python verfügt jedoch über viele andere Datenstrukturen, die diese Aufgabe mehr oder weniger auf ähnliche Weise erfüllen. Im letzten Kapitel des Kurses "Datentypen für die Datenwissenschaft" erfährst du zum Beispiel mehr über Zähler,
defaultdictsundnamedtuples.