.. include:: markup.rst ****************** Objektorientierung ****************** Einführung ========== In diesem Kapitel lernen wir eines der wichtigsten Programmierparadigmen - :mark:`die Objektorientierung`. Bereits im ersten Kapitel wurde diese angesprochen. **Das Problem** Zwei der wichtigsten Programmierparadigmen sind: * Imperative Programmierung (z.B. prozedural) * Objektorientierte Programmierung (:mark:`OOP`) Die Abbildung `Fahrzeuge`_ veranschaulicht beide Möglichkeiten. .. _`Fahrzeuge`: .. figure:: images/autos.png :scale: 100 % :align: center Unsortierte Fahrzeuge (imperativ, oben) und sortierte Fahrzeuge (OOP, unten) Frage: Suche ein blaues Auto ohne die anderen Fahrzeuge zu stören/berühren? Es ist klar: * Im Falle einer Sortierung geht es leichter, schneller * :mark:`aber` die Sortierung braucht vorab Zeit! Das ist das :mark:`Grundprinzip` der OOP! **Grundidee** * Eine Software wird in Grundstrukturen zerlegt welche übersichtlich sind (Beispiel: Ein Auto besteht aus Motor, Chassis, Räder). * Diese Grundstrukturen nennt man :mark:`Klassen` (Beispiel: Rad, Motor, ... oder wie oben Auto, LKW, Bus) * Von so einer Klasse können beliebig viele :mark:`Objekte` instanziert werden. (Beispiel: Rad 1, Rad 2, Rad 3, ... oder wie oben Auto 1, Auto 2, Auto 3) * Objekte bestehen aus :mark:`Daten/Attributen` (z.B. Raddurchmesser, Automarke, ...) und :mark:`Funktionen/Methoden` (Berechne Radumfag, Sag die Automarke, ... ), welche diese modifizieren, verarbeiten oder zurückgegeben. **Grundbestandteile der OOP** * Datenkapselung * Vererbung * Polymorphismus Im folgenden werden diese Dinge im Detail erklärt. **Vorteile OOP** * Modularität (Wartbarkeit, leichter Fehler finden) * Verbergen von Informationen (man wird nicht "erschlagen" beim Code lesen) * Wiederverwendbarkeit von Code (nicht mehrmals gleiche Funktion implementiert) * Leichte Erweiterbarkeit (andere Programmteile werden nicht beeinflusst) **Nachteile OOP** * Schwieriger und aufwendiger zu erstellen (Design + Implementierung) * eventuell langsamere Ausführung (je nach Implementierung) Prozedurales Beispiel: Konto ---------------------------- Am besten :mark:`versteht` man diese Dinge :mark:`an einem Beispiel`. Stellen wir uns einmal vor, wir würden für eine Bank ein System für die :mark:`Verwaltung von Konten` entwickeln, das das Anlegen neuer Konten, Überweisungen sowie Ein- und Auszahlungen ermöglicht. Ein möglicher Ansatz sähe so aus, dass wir für jedes Bankkonto ein :mark:`Dictionary` anlegen, in dem dann alle Informationen über den Kunden und seinen Finanzstatus gespeichert sind. Um die gewünschten Operationen zu unterstützen, würden wir Funktionen definieren. Ein Dictionary für ein stark vereinfachtes Konto könnte folgendermaßen aussehen: :: konto = { "Inhaber" : "Hans Meier", "Kontonummer" : 5671, "Kontostand" : 12000.0, } Mit unserem jetzigen Wissen könnte ein :mark:`prozedurales` Programm wie folgt aussehen (``oop_konto_dict.py``): .. literalinclude:: src/oop_konto_dict.py Führt man dieses Programm mit ``python konto_dict.py`` aus erhält man :: Kontobeispiel mit dict .. neues Konto : Heinz Meier .. neues Konto : Erwin Schmidt .. Transfer : Heinz Meier -> Erwin Schmidt 100 .. Auszahlen : Heinz Meier 200 .. Einzahlen : Erwin Schmidt 500 .. Konto : Heinz Meier Kontonummer : 1234 Kontostand : 11700.0 .. Konto : Erwin Schmidt Kontonummer : 6789 Kontostand : 15600.0 Erklärung dazu: * Das Hauptprogramm startet mit ``if __name__ == '__main__':``. Dieser Anweisungskörper wird nur ausgeführt wenn das File mit ``python konto_dict.py`` gestartet wird (=Hauptprogramm). Im Falle eines Imports d.h. ``import konto_dict.py`` würde alles nach dem ``if`` nicht ausgeführt werden. * Zunächst erzeugt man zwei Konten ``k1`` und ``k2``. * Die Funktion ``neues_konto`` retourniert ein Dictionary mit den entsprechenden Einträgen bei der Erzeugung. * Die Funktion ``ueberweisung`` benötig ein Quell- und Zielkonto. Diese erhöht bzw. erniedrigt den Kontostand. * Die beiden Funktionen ``einzahlen`` und ``auszahlen`` machen dies an einem Konto. * Mit ``zeige_konto`` kann man sich die Kontodaten ausgeben lassen. .. note:: Jede Funktion hat ein ``print`` damit man beim Programmlauf sieht was passiert! Die Banksimulation arbeitet wie erwartet. Sie weist aber einige :mark:`unschöne Eigenheiten` auf: * Bei den :mark:`Funktionsaufrufen` müssen immer :mark:`Daten übergeben` werden. Problematisch bei großen Datenmengen. * Der Kontostand (``dict``) kann im Hauptprogramm direkt verändert werden ohne dass man dies merkt. Diese umgeht man mit der :mark:`Datenkapselung` bei der OOP (siehe unten). * Man kann Kontos nicht zusammenzuführen z.B. ``k1+k2``. In der OOP möglich und bekannt als :mark:`Polymorphismus` (Überladung des ``+`` Operators). * Es ist nicht möglich von einem Basiskonto andere Kontotypen wie Sparkonto, Girokonto, etc abzuleiten. Dies kennt man als :mark:`Vererbung` in der OOP . Im Folgenden wird diese Objektorientierung am gleichen Beispiel erklärt. Klassen ======= Konto Beispiel OOP ------------------ Zu Beginn des objektorientierten Designs zeichnet man i.A. die Klassen graphisch mittels :mark:`UML` (Unified Modelling Language) auf: Die Abbildung `Konto`_ zeigt dies für unser Beispiel. .. _`Konto`: .. figure:: images/Konto.png :scale: 80 % :align: center UML Darstellung einer einfachen Konto Klasse ``Konto`` ist dabei der Klassenname. Danach folgen die :mark:`Attribute` (Daten) und :mark:`Methoden` (Funktionen). Basierend darauf würde eine :mark:`Prototyp-Klasse` in Python wie folgt aussehen (``oop_konto_pass.py``): .. literalinclude:: src/oop_konto_pass.py Durch die ``pass`` :mark:`Anweisung` kann man die Methoden angeben, ohne diese implementieren zu müssen. Als ersten Schritt führen Sie dieses Skript in Spyder mittels ``run`` aus, um die Klasse interaktiv verwendbar zu haben. Es erscheint in der IPython-Konsole: :: In [1]: runfile('C:/Users/.../src/oop_konto_pass.py', wdir='C:/Users/.../src') Anmerkungen zum Beispiel: * Die Definition einer Klasse startet mit ``class`` und dem Namen der Klasse. * Die erste Methode ``__init__`` ist der sogenannte :mark:`Konstruktor`. Die beiden ``__`` deuten auf eine spezielle Python Methode hin. Diese wird aufgerufen wenn ein neues Objekt dieser Klasse z.B. in der IPython-Konsole erzeugt wird z.B. mit: :: In [2]: k1 = Konto("Heinz Meier", 1234, 12000.0) In [3]: k1 Out[3]: <__main__.Konto at 0x2360f8a7f48> und erledigen i.A. erforderliche Variablenzuweisungen. * Danach kommen (eingerückt) die Methoden. Diese werden mit ``objektname.methodename`` aufgerufen z.B. mit :: In [4]: k1.auszahlen(200) Anmerkung: durch die ``pass`` Anweisung wird der Kontostand nicht verändert. * Jede Funktionsdefinition hat einen ``self``-Parameter. Dieser übergibt automatisch eine Referenz auf die Instanz bei einem Methodenaufruf. Bei: :: In [5]: k1.auszahlen(300) werden der Funktion zwei Argumente (``k1`` und ``200``) übergeben. ``k1`` entspricht dabei ``self``. In Sprachen wie C++ nennt man diesen Zeiger ``this``-pointer. * Der Konstruktor setzt die Attribute (Member) der Klasse. Diese sind öffentlich (:mark:`public`) und man kann diese anzeigen und verändern: :: In [6]: k1.Inhaber Out[6]: 'Heinz Meier' In [7]: k1.Inhaber = "Karli" # neuer Inhaber In [8]: k1.Inhaber Out[8]: 'Karli' * Um dieses Attribute (Member) wirklich Privat (:mark:`private`) zu machen, müssen im Konstruktor die Attributenamen mit ``__`` beginnen d.h.: :: self.__Inhaber = inhaber Das vollständige mit privaten Attributen Kontobeispiel in objektorientiertem Design lautet (``oop_konto_class_einfach.py``): .. literalinclude:: src/oop_konto_class_einfach.py und erzeugt folgende Ausgabe: :: Kontobeispiel mit class .. Transfer : Heinz Meier -> Erwin Schmidt 100 .. Auszahlen : Heinz Meier 200 .. Einzahlen : Erwin Schmidt 500 .. Konto : Heinz Meier Kontonummer : 1234 Kontostand : 11700.0 .. Konto : Erwin Schmidt Kontonummer : 6789 Kontostand : 15600.0 Anmerkungen zum Beispiel: * Alle Members sind privat. Nur so hat man eine :mark:`Datenkapselung`! * Nach der Klassen- bzw. Methodendefinition steht ein Docstring. Diesen kann man anzeigen mit :: In [2]: k1.__doc__ Out[2]: ' Beispiel eines einfachen Kontos ' oder :: In [3]: k1.einzahlen.__doc__ Out[3]: ' Mach eine Einzahlung ' Dadurch kann man den Code gut dokumentieren! Set und Get Methoden -------------------- Angenommen man möchte nur den :mark:`Namen des Kontoinhabers` abfragen bzw. dieser möchte seinen Namen ändern. Dazu benötigt man sogenannte ``set`` und ``get`` Methoden. Man erweitert die obige Klasse mit (siehe ``oop_konto_class_getset.py``): :: def inhaber(self): """ Gibt den Namen des Inhabers zurueck """ print(".. getter wird aufgerufen") return self.__Inhaber def setInhaber(self, neuer_Inhaber): """ Aendert den Namen des Inhabers """ print(".. setter wird aufgerufen") self.__Inhaber = neuer_Inhaber und kann dadurch auf die Attribute zugreifen (nachdem das Skript in Spyder ausgeführt wurde). :: In [2]: k1.inhaber() .. getter wird aufgerufen Out[2]: 'Dipl.-Ing. Hans Meier' In [3]: k1.setInhaber("Heinz Meier") .. setter wird aufgerufen In [4]: k1.inhaber() .. getter wird aufgerufen Out[4]: 'Heinz Meier' **property-Attribut** : Um das Ganze etwas eleganter zu machen, kann man beide Funktionen mit ``property`` "zusammenhängen" (vollständiges Beispiel siehe ``oop_konto_class_property.py``): :: class Konto(): def inhaber(self): """ Gibt den Namen des Inhabers zurueck """ print(".. getter wird aufgerufen") return self.__Inhaber def setInhaber(self, neuer_Inhaber): """ Aendert den Namen des Inhabers """ print(".. setter wird aufgerufen") self.__Inhaber = neuer_Inhaber # Property-Attribut Inhaber = property(inhaber, setInhaber) Der Zugriff sieht dann folgendermaßen aus: :: In [2]: k1 = Konto("Heinz Meier", 1234, 12000.0) In [3]: k1.Inhaber .. getter wird aufgerufen Out[3]: 'Heinz Meier' In [4]: k1.Inhaber = "Dipl.-Ing. Heinz Meier" .. setter wird aufgerufen In [5]: k1.Inhaber .. getter wird aufgerufen Out[5]: 'Dipl.-Ing. Heinz Meier' .. note:: Man könnte nun sagen dies ist das Gleiche als mit einem :mark:`public` Attribut. Der Unterschied: es wird die ``Inhaber()`` bzw. ``setInhaber()`` Methode aufgerufen. Darin könnte man z.B. Zugriffsberechtigungen definieren! Statische Attribute ------------------- Alle bislang kennengelernten Attribute waren :mark:`dynamisch`, d.h. diese werden bei der Objekterzeugung bzw. Entfernung automatisch erzeugt bzw. gelöscht. Möchte man im vorliegenden Beispiel einen Konto-Zähler einbauen welcher die Anzahl der Objekte speichert, braucht man ein :mark:`statisches` Attribut (siehe ``oop_konto_class_static.py``): :: class Konto(): """ Beispiel eines Kontos """ # Statischer Zaehler Anzahl = 0 def __init__(self, inhaber, kontonummer, kontostand): """ Konstruktor, Aufruf bei Instanzierung """ self.__Inhaber = inhaber self.__Kontonummer = kontonummer self.__Kontostand = kontostand Konto.Anzahl += 1 # Instanzzaehler erhoehen def __del__(self): """ Destruktor, Aufruf bei del """ Konto.Anzahl -= 1 Anmerkungen zum Beispiel: * Das Attribut ``Anzahl`` steht :mark:`direkt nach` der ``class`` :mark:`Definition` außerhalb der ``def`` Anweisungen. * Die Methode ``__del__`` ist der sogenannte Destruktor. Dieser wird beim Löschen eines Objektes aufgerufen. * Der Destruktor wir i.A. nicht implementiert, da Python alle Attribute automatisch löscht. Im Falle des Zählers ist diese Definition aber notwendig. Somit hat man einen statischen Zähler implementiert und kann diesen ausprobieren. Das Hauptprogramm dazu lautet: :: if __name__ == '__main__': print("\nKontobeispiel mit class & statischen Attributen") # Erzeuge zwei Konto-Objekte k1 = Konto("Heinz Meier", 1234, 12000.0) k2 = Konto("Erwin Schmidt", 6789, 15000.0) k3 = Konto("Susi Jung", 9147, 9000.0) # Anzahl bestehender Konten print(".. Kontoanzahl :", Konto.Anzahl) # Loesche ein Konto print("-> Loesche ") k3.zeige_konto() del(k3) # neue Anzahl print(".. Kontoanzahl :", Konto.Anzahl) Startet man das Skript ``oop_konto_class_static.py`` erhält man: :: Kontobeispiel mit class & statischen Attributen .. Kontoanzahl : 3 -> Loesche .. Konto : Susi Jung Kontonummer : 9147 Kontostand : 9000.0 .. Kontoanzahl : 2 Vererbung ========= Im folgenden wird das obige Programm erweitert, um das Prinzip der Vererbung darzustellen. Es gibt bekanntlich :mark:`unterschiedliche Kontotypen` (Girokonto, Sparkonto, ...). Alle haben * :mark:`Gemeinsamkeiten` aber auch * :mark:`unterschiedliche Eigenschaften`. Dieses Verhalten kann man :mark:`durch Vererbung realisieren`. Die Abbildung `Vererbung`_ zeigt dies für unser Beispiel. .. _`Vererbung`: .. figure:: images/Konto_Vererbung.png :scale: 80 % :align: center UML Darstellung einer einfachen Vererbung Der dazugehörige vereinfachte Code lautet (``oop_konto_vererbung.py``): .. literalinclude:: src/oop_konto_vererbung.py :language: python Dieses Programm liefert folgenden Output: :: Kontobeispiel mit Vererbung .. Konto anlegen .. Konto : Heinz Meier Kontonummer : 78340 Kontostand : 12000.0 .. Konto anlegen .. Konto : Heinz Meier Kontonummer : 78341 Kontostand : 4000.0 Zinssatz : 0.03 ------------------------- .. Einzahlen : 78340 1000.0 .. Einzahlen : 78341 2000.0 .. Auszahlen : 78340 300.0 .. Transfer : 78340 -> 78341 100.0 ------------------------- .. Konto : Heinz Meier Kontonummer : 78340 Kontostand : 12600.0 .. Konto : Heinz Meier Kontonummer : 78341 Kontostand : 6100.0 Zinssatz : 0.03 Anmerkungen zum Beispiel: * Durch die Klassendefinition ``class Neueklasse(Basisklasse)`` werden der neuen Klasse alle Methoden der Basisklasse vererbt. * Im Konstruktor-Aufruf der neuen Klasse wird mit ``Konto.__init__()`` der Konstruktor der Basisklasse aufgerufen. * Die Methoden ``einzahlen()`` und ``auszahlen()`` wurden vererbt und nicht neu implementiert. * Nur die Klasse ``Girokonto`` hat eine ``ueberweisung()`` Funktion. * Bei der Klasse ``Sparkonto`` wurde exemplarisch die ``zeige_konto()`` Methode überschrieben. Gleiches könnte man auch bei der Klasse ``Girokonto`` machen. * Set und Get Methoden wurden der :mark:`Übersichtlichkeit` halber nicht implementiert. Polymorphismus ============== Polymorphismus (griechisch, „Vielgestaltigkeit“) ist ein Konzept in der Programmierung * mit ein und der :mark:`selben Funktion` (z.B. ``print``) bzw. selben Operatoren (``+``,``-``,``*``,...) * :mark:`verschiedene Datentypen` verwenden zu können. Hier ein einfaches Beispiel (``oop_vektor.py``): .. literalinclude:: src/oop_vektor.py :language: python mit der zugehörigen Ausgabe: :: Beispiel Polymorphismus vs = [4 6] Anmerkungen zum Beispiel: * Der ``__add__`` Operator (``+``) und ``__str__`` (steuert ``print`` Ausgabe) werden überlagert. * Dies erlaubt in wenigen Zeilen die Definition eines eigenen Datentyps ``Vektor``. * Sowie die Verwendung von ``+`` und ``print``. * Es gibt eine Vielzahl von sogenannten :mark:`Magic Members` welche überladen werden können. Dazu gehören z.B.: :: __init__() __del__() __str__() __iter__() __add__() __sub__() __mul__() __gt__() Übungsbeispiele ================= **Aufgabe 8.1** Schreiben Sie eine Klasse ``Vektor``, die einen Vektor beliebiger Dimension speichern kann. Als Klassenattribute sollen eine Liste ``Liste`` und die Dimension der Liste ``Dim`` gespeichert werden. 1. Der Konstruktor bekommt eine Liste übergeben und initialisiert damit die Attribute ``Liste`` und ``Dim``. 2. Definieren Sie eine Methode ``norm()``, die die euklidische Norm des Vektors berechnet: .. math:: ||\mathbf{v}|| = \sqrt{ \sum_{i=1}^n v_i^2 } 3. Überladen Sie den Additions-Operator (``__add__``), sodass 2 Vektoren addiert werden und als Resultat ein neuer Vektor zurück gegeben wird. Die Addition soll für beliebige Dimensionen funktionieren. Sie können annehmen, dass die beiden Vektoren die selbe Dimension haben. z.B.: :: In [1]: v = Vektor([1, 2]) + Vektor([3, 4]) In [2]: print(v.Liste, v.Dim) [4, 6] 2 4. Machen Sie das Attribut ``Dim`` privat. Was müssen Sie dadurch zusätzlich an Ihrem Programm ändern? Testen Sie Ihre Klasse auch mit Vektoren größerer Dimensionen! **Aufgabe 8.2** Schreiben Sie eine Klasse ``Kraft``, die die Masse ``m`` und den Beschleunigungsvektor ``a`` übergeben bekommt und den daraus resultierenden Kraftvektor berechnet (f = m * a). Als Attribute sollen * die Masse ``m`` (Gleitkommazahl), * der Beschleunigungsvektor ``a`` (Liste beliebiger Dimension) * die resultierende Kraft ``Liste`` (Liste beliebiger Dimension) und * die Dimension ``Dim`` von ``Liste`` (Ganzzahl) gespeichert werden. 1. Dazu soll die Klasse ``Kraft`` von der Klasse ``Vektor`` abgeleitet werden: Sie soll alle Attribute und Methoden von der Klasse ``Vektor`` erben. Zusätzlich sollen die Attribute ``Liste`` und ``Dim`` über den Konstruktor der Klasse ``Vektor`` gesetzt werden. 2. Überladen Sie den print-Operator (``__str__``), sodass die Masse, der Beschleunigungsvektor und der Kraftvektor ausgegeben werden. 3. Überprüfen Sie die Ergebnisse der Methode ``norm()`` und des überladenen Additions-Operators der Klasse ``Kraft``. weitere Übungsbeispiele ================= **Aufgabe 8.3** Modifizieren Sie die Lösung von Aufgabe 8.1 so, dass auch der Multiplikations-Operator (``__mul__``) überladen wird. Das Produkt ``Vektor1 * Vektor2`` soll dabei das Skalarprodukt der Instanzen ``Vektor1`` und ``Vektor2`` berechnen. z.B.: :: In[1]: print(Vektor([1, 2]) * Vektor([4, 6])) 16 **Aufgabe 8.4** Erstellen Sie aufbauend auf den Funktionen aus Aufgabe 7.1 eine Klasse: 1. Definieren Sie eine Klasse mit passendem Namen. Die Klasse soll die Datenstruktur zum Speichern der Länder (z.B. ein Dictionary) als Klassenattribut enthalten. Dazu soll ein Konstruktor ohne Parameter definiert werden, der das Dictionary initialisiert. 2. Wandeln Sie alle Funktionen von Aufgabe 7.1 zu Klassenmethoden um. 3. Überladen Sie den print-Operator, sodass das Dictionary der Klasse direkt mittels print auf eine Klasseninstanz ausgegeben werden kann. **Aufgabe 8.5** 1. Erstellen Sie eine Klasse ``Person`` mit den Attributen ``Name`` (string) und ``Alter`` (integer). Die Attribute sollen über den Konstruktor gesetzt und als private (!) Variablen gespeichert werden. z.B.: :: In [1]: Anton = Person("Anton Mueller", 23) 2. Schreiben Sie Get-Methoden, über die das Alter und der Name der Person abgerufen werden können. Schreiben Sie eine Methode zum Setzen des Alters der Person. Überprüfen Sie in einem Abschnitt ``main``, dass tatsächlich nicht direkt auf die Attribute Alter und Name zugegriffen werden kann. z.B. für die oben definierte Person ``Anton``: :: In [2]: print(Anton.Get_Alter()) 23 In [3]: print(Anton.Get_Name()) Anton Mueller In [4]: Anton.Set_Alter(40) In [5]: print(Anton.Get_Alter()) 40 3. Erstellen Sie eine von ``Person`` abgeleitete Klasse ``Student``, welche neben den Attributen ``Alter`` und ``Name`` noch das zusaetzliche Attribut ``Matrikelnummer`` (= string) hat. Die Matrikelnummer soll NICHT als private Variable gespeichert werden! Die Attribute sollen in einem Konstruktor gesetzt werden. Verwenden Sie zum Initialisieren der Attribute ``Alter`` und ``Name`` den Konstruktor der Basisklasse ``Person``! z.B.: :: In [6]: Student_Anton = Student("Anton Mueller", 23, "1110111") 4. Überladen Sie für die Klasse ``Student`` den print-Operator, so dass Name, Alter und Matrikelnummer des Studenten ausgegeben werden. Verwenden Sie zum Abrufen des Namens und Alters die fuer 2.) geschriebenen Get-Methoden. z.B. für den oben definierten Studenten ``Student_Anton``: :: In [7]: print(Student_Anton) Name: Anton Mueller, Alter: 23, Matrikelnummer: 1110111