LU02e - By Value und By Reference bei Dataclasses und Objekten in Python
Einführung
In Python werden Objekte, einschließlich Dataclasses, immer By Reference übergeben. Das bedeutet, dass bei der Übergabe eines Objekts an eine Funktion eine Referenz auf das Objekt übergeben wird und keine Kopie davon erstellt wird. Änderungen, die innerhalb der Funktion an den Attributen des Objekts vorgenommen werden, wirken sich direkt auf das ursprüngliche Objekt aus.
Animation: Objekte, frozen und Standardwerte
Die Animation zeigt drei Abläufe Schritt für Schritt. Tab 1 und 2 gehören zu den Beispielen weiter unten; Tab 3 zeigt, was bei einem veränderbaren Standardwert passiert – und damit den Grund, weshalb Dataclasses solche Standardwerte gar nicht erst zulassen.
Dataclasses
Dataclasses wurden in Python 3.7 eingeführt und bieten eine bequeme Möglichkeit, Klassen zu definieren, die hauptsächlich Daten speichern. Sie reduzieren den Boilerplate-Code, der normalerweise mit der Definition von Klassen einhergeht, und bieten gleichzeitig eine klare und strukturierte Art der Datenverwaltung.
Hier ein einfaches Beispiel für eine Dataclass:
from dataclasses import dataclass @dataclass class Person: name: str age: int
Diese Dataclass Person hat zwei Attribute: name und age.
By Reference bei Dataclasses
Da Dataclasses Objekte in Python sind, werden sie By Reference übergeben. Das bedeutet, dass jede Veränderung der Attribute eines Dataclass-Objekts innerhalb einer Funktion direkt auf das Originalobjekt wirkt.
Beispiel: By Reference mit Dataclasses
from dataclasses import dataclass @dataclass class Person: name: str age: int def birthday(person: Person): """ Increments the age of the person by 1. """ person.age += 1 if __name__ == '__main__': p = Person(name='Alice', age=30) print(f'Vor der Änderung: {p}') # Output: Vor der Änderung: Person(name='Alice', age=30) birthday(p) print(f'Nach der Änderung: {p}') # Output: Nach der Änderung: Person(name='Alice', age=31)
In diesem Beispiel wird die Instanz der Dataclass Person an die Funktion birthday() übergeben. Da Objekte By Reference übergeben werden, wird das Attribut age der Originalinstanz p direkt in der Funktion verändert.
Mutable und Immutable Attribute in Dataclasses
Python-Dataclasses können sowohl mutable als auch immutable Attribute enthalten. Unabhängig davon, ob die Attribute mutable oder immutable sind, wird die Dataclass selbst immer By Reference übergeben.
Mutable Standardwerte sind nicht erlaubt
Es liegt nahe, einem Listen-Attribut einfach eine leere Liste als Standardwert zu geben:
from dataclasses import dataclass @dataclass class Student: name: str grades: list = [] # ValueError!
Dieser Code lässt sich nicht ausführen. Bereits beim Definieren der Klasse bricht Python ab:
ValueError: mutable default <class 'list'> for field grades is not allowed: use default_factory
Der Grund: Ein Standardwert wird einmalig ausgewertet – nämlich dann, wenn die Klasse bzw. die Funktion definiert wird, und nicht bei jeder Instanzierung. Alle Instanzen würden sich also dieselbe Liste teilen. Weil das fast nie gewollt ist, verbieten Dataclasses solche Standardwerte von vornherein.
Bei normalen Funktionen greift dieser Schutz nicht! Dort ist derselbe Fehler erlaubt – und still:
def add_grade(grade, grades=[]): grades.append(grade) return grades print(add_grade(90)) # Output: [90] print(add_grade(80)) # Output: [90, 80] <- und nicht [80]
Die Liste im Standardwert überlebt den Funktionsaufruf, weil sie nur einmal erzeugt wurde. Tab 3 der Animation oben spielt genau das durch. Die Lösung bei Funktionen: None als Standardwert verwenden und die Liste im Funktionsrumpf erzeugen.
Lösung: Verwendung von `field(default_factory=list)`
Für mutable Standardwerte verwendet man in Dataclasses die Funktion field mit dem Parameter default_factory. Diese Factory wird bei jeder Instanzierung neu aufgerufen – dadurch erhält jede Instanz ihre eigene Liste.
from dataclasses import dataclass, field @dataclass class Student: name: str grades: list = field(default_factory=list) if __name__ == '__main__': student1 = Student(name='Alice') student2 = Student(name='Bob') student1.grades.append(90) print(student2.grades) # Output: [] - Jede Instanz hat ihre eigene, unabhängige Liste
Immutable Dataclasses
Wenn Sie möchten, dass eine Dataclass unveränderlich ist, können Sie sie durch Setzen des frozen-Parameters in @dataclass unveränderlich machen. In einer gefrorenen (frozen) Dataclass sind alle Attribute unveränderlich, und jede versuchte Änderung führt zu einem Fehler. Dies ähnelt dem Verhalten von By Value, da die ursprüngliche Instanz nicht verändert werden kann.
Beispiel: Immutable Dataclass
from dataclasses import dataclass @dataclass(frozen=True) class ImmutablePerson: name: str age: int
In diesem Fall ist die Dataclass ImmutablePerson unveränderlich, was bedeutet, dass jedes Attribut, sobald es gesetzt ist, nicht mehr geändert werden kann. Jeder Versuch, das Attribut age oder name zu ändern, führt zu einem FrozenInstanceError:
FrozenInstanceError: cannot assign to field 'age'
Beispiel: Anwendung einer frozen Dataclass
from dataclasses import dataclass @dataclass(frozen=True) class Person: name: str age: int def birthday(person: Person) -> Person: """ Returns a new Person instance with the age incremented by 1. """ return Person(name=person.name, age=person.age + 1) if __name__ == '__main__': p = Person(name='Alice', age=30) new_p = birthday(p) print(f'Original: {p}') # Output: Original: Person(name='Alice', age=30) print(f'Neue Instanz: {new_p}') # Output: Neue Instanz: Person(name='Alice', age=31)
Erläuterung:
Die Dataclass Person ist als frozen markiert, was sie unveränderlich macht.
Die Funktion birthday ist eine pure function. Sie nimmt eine Instanz der Dataclass Person als Eingabe und gibt eine neue Instanz zurück, wobei das age-Attribut um eins erhöht wird.
Das Originalobjekt p bleibt unverändert. Stattdessen wird eine neue Instanz new_p mit dem geänderten Alter erstellt.
Vorteile:
- Keine Seiteneffekte: Da das Originalobjekt nicht verändert wird, gibt es keine ungewollten Änderungen am Zustand.
- Vorhersehbarkeit: Pure functions sind leichter zu verstehen und zu testen, da das Ergebnis nur von den Eingabewerten abhängt.
- Immutable Data: Die Verwendung von immutable Datenstrukturen verhindert unbeabsichtigte Modifikationen und erleichtert die Parallelisierung von Programmen.
Dieses Vorgehen ist besonders nützlich in Szenarien, in denen Datenintegrität und Vorhersagbarkeit des Programmverhaltens von hoher Bedeutung sind.
Zusammenfassung
In Python werden Objekte, einschließlich Dataclasses, immer By Reference übergeben. Änderungen an den Attributen eines Objekts innerhalb einer Funktion wirken sich direkt auf das Originalobjekt aus. Um ungewollte Änderungen zu vermeiden, kann eine Dataclass als frozen deklariert werden, was sie unveränderlich macht und vor unbeabsichtigten Modifikationen schützt. Zusätzlich ist es wichtig, bei mutable Attributen wie Listen field(default_factory=list) zu verwenden – ein direkter mutable Standardwert wird von Dataclasses gar nicht erst akzeptiert.
Tipp: Wenn Sie mutable Attribute in einer Dataclass verwenden, sollten Sie sorgfältig überlegen, ob die direkte Veränderung dieser Attribute innerhalb von Funktionen in Ihrem Programmdesign erwünscht ist. Verwenden Sie frozen Dataclasses oder field(default_factory=list), um unbeabsichtigte Änderungen zu verhindern.
