Im vorherigen Blogbeitrag zu Klassen und Objekten sind wir bereits des Öfteren auf den ominösen Python self Parameter gestoßen. Was sich hinter diesem Parameter verbirgt und warum er so wichtig ist, wirst du in den kommenden Zeilen im Detail erfahren.
Inhaltsverzeichnis
1. Was ist eine Referenz?
Das heutige Thema wird für Einsteiger vermutlich nicht sehr einfach nachzuvollziehen sein, was allerdings ganz normal ist. Aus diesem Grund werden wir uns vorab noch im Detail ansehen, was eine sogenannte Referenz ist, sodass du anhand dieses Wissens den Python self Parameter am Ende verstehen kannst.
Was eine Referenz ist, können wir uns anhand des Beispiels aus dem letzten Artikel noch einmal bewusst machen. In diesem haben wir eine Klasse Car und darin die init-Methode definiert. Außerdem haben wir für die Klasse die drei Attribute car_brand, horse_power und color definiert. Infolgedessen haben wir zwei Objekte von der Klasse Car erzeugt und diese jeweils in den Variablen car1 und car2 gespeichert:
An dieser Stelle lassen wir uns einmal mithilfe der print-Funktion den konkreten Inhalt dieser Variablen ausgeben:
Wie du sehen kannst, erscheint nun diese kryptisch aussehende Ausgabe. Erwartet hätten wir wahrscheinlich eine schöne Auflistung der Attribute unserer Objekte.
Bei der kryptischen Zahl am Ende der Ausgabe handelt es sich um die sogenannte Referenz. Sobald wir ein Objekt erzeugen, reserviert der Computer Speicherplatz für dieses. Auf diesem reservierten Speicher speichert er dann die einzelnen Attribute und somit die Daten des Objekts. Schließlich müssen die Informationen an irgendeinem Ort abgelegt werden, damit man wieder darauf zugreifen kann.
In den Variablen, welchen wir die erzeugten Objekte zuweisen, speichert der Computer nun nicht die Attribute des Objekts, sondern die Adresse des Speicherbereichs. Ab dieser Adresse ist im Speicher das ganze Objekt also nacheinander abgespeichert.
Noch einmal kurz zur Wiederholung: Wenn wir ein Objekt erzeugen, wird intern Speicher reserviert. Bei diesem werden der Reihe nach alle Daten des Objekts abgespeichert. Die Adresse, die wir bei der Objekterzeugung bei der Zeile Car() zurückerhalten und die wir in der Variable car1 bzw. car2 speichern, ist die Anfangsadresse des Speicherbereichs, an dem das komplette Objekt letztendlich gespeichert wird. Und das bezeichnet man als Referenz.
Sehen wir uns das Ganze am besten noch einmal auf einer Skizze an:
Zunächst haben wir das Car Objekt 1 erzeugt. Dieses beinhaltet die Attribute car_brand, horse_power und color. Welche Werte genau darin gespeichert sind, ist erst mal nebensächlich.
Dieses Objekt liegt nun im Speicher an einer bestimmten Stelle. In der Variable car1 ist also nicht der Inhalt des ganzen Objekts gespeichert, sondern lediglich die Adresse, die dort hinführt, wo das Objekt vollständig im Speicher liegt. Genau deshalb sagt man: car1 beinhaltet eine Referenz auf dieses erzeugte Objekt. Und mit dem zweiten Objekt verhält es sich genauso.
Im weiteren Verlauf des Programms haben wir ein weiteres Car-Objekt erzeugt und das der Variable car2 zugewiesen. Auch dieses Objekt besitzt die Attribute car_brand, horse_power und color. An dieser Stelle ist nun wichtig zu verstehen, dass wir ein zweites neues Objekt erzeugt haben. Damit existieren im Programm also Objekt 1 und Objekt 2, welche vollkommen unabhängig voneinander sind.
Das bedeutet: Objekt 2 liegt an einer anderen Stelle im Speicher als Objekt 1. Diese entsprechende Speicheradresse ist in Variable car2 gespeichert.
2. Beispiel zur Verdeutlichung von Referenzen
Damit weißt du also schon einmal, was eine Referenz ist.
Jetzt setzen wir Folgendes um: Unter unseren Code schreiben wir zunächst car3. Anstatt nun ein drittes Objekt zu erzeugen, weisen wir car3 die Variable car1 zu und lassen uns car3 mit der print-Funktion ausgeben:
In der Ausgabe sehen wir nun Folgendes: Die ausgegebene Adresse von car1 ist exakt die gleiche wie die von car3. Das liegt daran, dass in car1 nur die Referenz auf das im Programm zuerst erzeugte Car-Objekt gespeichert ist. Diese Referenz haben wir nun zusätzlich auch der Variable car3 zugewiesen.
Das heißt in anderen Worten: Sowohl mit der Variable car1 als auch mit der Variable car3 greift man jetzt auf ein und dasselbe Objekt zu, weil in beiden Variablen die gleiche Adresse gespeichert ist und diese somit auf ein und dasselbe Objekt im Speicher referenziert.
Doch welche Auswirkungen hat das? Weiter oben im Code haben wir zum Beispiel das Attribut horse_power auf den Wert 250 gesetzt und es anschließend ausgegeben. Wenn wir nun unten im Programm diese print-Anweisung noch einmal hineinkopieren und das car1 durch car3 ersetzen, greifen wir auf das gleiche Attribut wie oben zu. Denn car1 und car3 referenzieren schließlich beide auf ein und dasselbe Objekt.
3. Funktionsweise und Nutzen des Python self Parameters
An dieser Stelle können wir den Bogen zurück auf den Python self Parameter schlagen, den wir hier als Parameter in der init-Methode definiert haben.
Die Aufgabe von self besteht darin, die Referenz des ausführenden Objekts zurückzuliefern.
Die Frage, die man sich nun stellt, ist wahrscheinlich: Warum benötigt man das?
Ganz einfach: Mit der Klasse, die wir oben definieren, geben wir ganz allgemein den Bauplan für unsere Objekte vor. Von dieser Klasse können wir aber, wie wir wissen, nicht nur ein einziges Objekt erzeugen, sondern beliebig viele!
In unserem Beispiel haben wir aktuell zwei Objekte von diesem Bauplan erzeugt. Es gibt also zwei unterschiedliche Speicherbereiche innerhalb des Speichers. Auf einem liegt das erste instanziierte Objekt vom Typ Car und an einer anderen Stelle befindet sich das zweite instanziierte Objekt vom Typ Car.
Was wir gelernt haben, ist, dass das Programm beim Instanziieren eines Objekts immer automatisch die init-Methode aufruft. Innerhalb dieser init-Methode setzen wir die Werte für die Attribute. Die Programmausführung muss jetzt allerdings wissen, auf welchem konkreten Objekt sie aktuell arbeitet, sobald sie sich innerhalb der init-Methode befindet.
Schließlich kann es mehrere Objekte vom Typ Car innerhalb des Programms geben. Wenn das Programm also nicht wüsste, auf welchem Objekt es aktuell arbeitet, könnte es passieren, dass es von einem anderen Objekt ungewollt den Wert von horse_power auf None setzt. Aber es soll eben genau dieses Objekt sein, welches wir gerade erzeugen.
Genau deshalb existiert das self in Python. Dort wird nämlich automatisch beim Aufruf der init-Methode die Referenz auf das aktuelle Objekt mitübergeben. Mit automatisch meine ich, dass wir bei der konkreten Objekterzeugung den Parameter, also die Referenz, nicht selbst mitübergeben müssen. Und das, obwohl in der init-Methode der Python self Parameter gefordert wird.
Das Programm lädt also die Referenz vom aufrufenden Objekt automatisch im Hintergrund in den Parameter self. Und das, ohne dass wir diesen explizit übergeben müssen.
Aus diesem Grund verwenden wir auch innerhalb der init-Methode beim Festlegen der Attribute das self gefolgt von einem Punkt und dem entsprechenden Attribut.
Im Falle mehrerer Objekte gibt es das Attribut schließlich öfter. Da aber self eine Referenz auf das aufrufende Objekt beinhaltet, kann Python eindeutig zuordnen, welches Objekt welches Attribut konkret ändern soll.
An dieser Stelle möchte ich dir noch einmal sagen, dass es sich hier um ein anfangs ziemlich schwieriges Thema handelt, das alles andere als leicht zu verstehen ist. Solltest du es also zu diesem Zeitpunkt noch nicht so wirklich verstanden haben, lies den Artikel am besten erneut oder sieh dir das Video dazu an. So kannst du noch einmal jeden aufgezeigten Schritt gedanklich selbst durchgehen. Sobald du es wirklich verinnerlicht hast, kannst du tatsächlich nachvollziehen, was im Hintergrund geschieht und weshalb das so wichtig ist.