8. Objektorientierung

8.1. Einführung

In diesem Kapitel lernen wir eines der wichtigsten Programmierparadigmen - die Objektorientierung. Bereits im ersten Kapitel wurde diese angesprochen.

Das Problem

Zwei der wichtigsten Programmierparadigmen sind:

  • Imperative Programmierung (z.B. prozedural)
  • Objektorientierte Programmierung (OOP)

Die Abbildung Fahrzeuge veranschaulicht beide Möglichkeiten.

_images/autos.png

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
  • aber die Sortierung braucht vorab Zeit!

Das ist das 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 Klassen (Beispiel: Rad, Motor, ... oder wie oben Auto, LKW, Bus)
  • Von so einer Klasse können beliebig viele Objekte instanziert werden. (Beispiel: Rad 1, Rad 2, Rad 3, ... oder wie oben Auto 1, Auto 2, Auto 3)
  • Objekte bestehen aus Daten/Attributen (z.B. Raddurchmesser, Automarke, ...) und 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)

8.1.1. Prozedurales Beispiel: Konto

Am besten versteht man diese Dinge an einem Beispiel.

Stellen wir uns einmal vor, wir würden für eine Bank ein System für die 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 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 prozedurales Programm wie folgt aussehen (oop_konto_dict.py):

def neues_konto(inhaber, kontonummer, kontostand): 
    print(".. neues Konto :", inhaber)
    return { 
        "Inhaber" : inhaber, 
        "Kontonummer" : kontonummer, 
        "Kontostand" : kontostand
        }
               
def ueberweisung(quelle, ziel, betr): 
    print(".. Transfer    :", quelle["Inhaber"], "->", ziel["Inhaber"], betr)
    quelle["Kontostand"] -= betr 
    ziel["Kontostand"] += betr 

def einzahlen(konto, betrag): 
    print(".. Einzahlen   :", konto["Inhaber"], betrag)
    konto["Kontostand"] += betrag 

def auszahlen(konto, betrag): 
    print(".. Auszahlen   :", konto["Inhaber"], betrag)
    konto["Kontostand"] -= betrag 

def zeige_konto(konto): 
    print(".. Konto       :", konto["Inhaber"])
    print("   Kontonummer :", konto["Kontonummer"])    
    print("   Kontostand  :", konto["Kontostand"])


if __name__ == '__main__':
    
    print("\nKontobeispiel mit dict")
    
    k1 = neues_konto("Heinz Meier", 1234, 12000.0) 
    k2 = neues_konto("Erwin Schmidt", 6789, 15000.0) 
   
    ueberweisung(k1, k2, 100)   
    auszahlen(k1, 200)
    einzahlen(k2, 500) 
    
    zeige_konto(k1)    
    zeige_konto(k2) 

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.

Bemerkung

Jede Funktion hat ein print damit man beim Programmlauf sieht was passiert!

Die Banksimulation arbeitet wie erwartet. Sie weist aber einige unschöne Eigenheiten auf:

  • Bei den Funktionsaufrufen müssen immer 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 Datenkapselung bei der OOP (siehe unten).
  • Man kann Kontos nicht zusammenzuführen z.B. k1+k2. In der OOP möglich und bekannt als Polymorphismus (Überladung des + Operators).
  • Es ist nicht möglich von einem Basiskonto andere Kontotypen wie Sparkonto, Girokonto, etc abzuleiten. Dies kennt man als Vererbung in der OOP .

Im Folgenden wird diese Objektorientierung am gleichen Beispiel erklärt.

8.2. Klassen

8.2.1. Konto Beispiel OOP

Zu Beginn des objektorientierten Designs zeichnet man i.A. die Klassen graphisch mittels UML (Unified Modelling Language) auf: Die Abbildung Konto zeigt dies für unser Beispiel.

_images/Konto.png

UML Darstellung einer einfachen Konto Klasse

Konto ist dabei der Klassenname. Danach folgen die Attribute (Daten) und Methoden (Funktionen). Basierend darauf würde eine Prototyp-Klasse in Python wie folgt aussehen (oop_konto_pass.py):

class Konto:

   def __init__(self, inhaber, kontonummer, kontostand):
      """ Konstruktor """
      self.Inhaber = inhaber
      self.Kontonummer = kontonummer
      self.Kontostand = kontostand

   def ueberweisung(self, ziel, betrag):
      pass

   def einzahlen(self, betrag):
      pass

   def auszahlen(self, betrag):
      pass

   def zeige_konto(self):
      pass

Durch die pass 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 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 (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 (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):

class Konto:
   """ Beispiel eines einfachen Kontos """

   def __init__(self, inhaber, kontonummer, kontostand):
      """ Konstruktor """
      self.__Inhaber = inhaber
      self.__Kontonummer = kontonummer
      self.__Kontostand = kontostand

   def ueberweisung(self, ziel, betrag):
      """ Mach eine Ueberweisung """
      print(".. Transfer    :", self.__Inhaber, "->", ziel.__Inhaber, betrag)
      self.__Kontostand -= betrag
      ziel.__Kontostand += betrag

   def einzahlen(self, betrag):
      """ Mach eine Einzahlung """
      print(".. Einzahlen   :", self.__Inhaber, betrag)
      self.__Kontostand += betrag

   def auszahlen(self, betrag):
      """ Mach eine Auszahlung """
      print(".. Auszahlen   :", self.__Inhaber, betrag)
      self.__Kontostand -= betrag

   def zeige_konto(self):
      """ Zeige die Kontodaten am Bildschirm """
      print(".. Konto       :", self.__Inhaber)
      print("   Kontonummer :", self.__Kontonummer)
      print("   Kontostand  :", self.__Kontostand)


if __name__ == '__main__':

   print("\nKontobeispiel mit class")

   # Erzeuge zwei Konto-Objekte
   k1 = Konto("Heinz Meier", 1234, 12000.0)
   k2 = Konto("Erwin Schmidt", 6789, 15000.0)

   # Mach was damit ...
   k1.ueberweisung(k2, 100)
   k1.auszahlen(200)
   k2.einzahlen(500)

   # Zeige Kontodaten am Bildschirm
   k1.zeige_konto()
   k2.zeige_konto()

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 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!

8.2.2. Set und Get Methoden

Angenommen man möchte nur den 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'

Bemerkung

Man könnte nun sagen dies ist das Gleiche als mit einem public Attribut. Der Unterschied: es wird die Inhaber() bzw. setInhaber() Methode aufgerufen. Darin könnte man z.B. Zugriffsberechtigungen definieren!

8.2.3. Statische Attribute

Alle bislang kennengelernten Attribute waren 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 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 direkt nach der class 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

8.3. Vererbung

Im folgenden wird das obige Programm erweitert, um das Prinzip der Vererbung darzustellen. Es gibt bekanntlich unterschiedliche Kontotypen (Girokonto, Sparkonto, ...). Alle haben

  • Gemeinsamkeiten aber auch
  • unterschiedliche Eigenschaften.

Dieses Verhalten kann man durch Vererbung realisieren. Die Abbildung Vererbung zeigt dies für unser Beispiel.

_images/Konto_Vererbung.png

UML Darstellung einer einfachen Vererbung

Der dazugehörige vereinfachte Code lautet (oop_konto_vererbung.py):

class Konto: 
    """ Basis Konto Klasse """

    def __init__(self, inhaber, kontonummer, kontostand): 
        """ Konstruktor, Aufruf bei Instanzierung """                
        print(".. Konto anlegen")
        self.Inhaber = inhaber 
        self.Kontonummer = kontonummer 
        self.Kontostand  = kontostand 
        self.zeige_konto()

    def einzahlen(self, betrag): 
        """ Mach eine Einzahlung """
        print(f".. Einzahlen   : {self.Kontonummer} {betrag:.1f}")
        self.Kontostand  += betrag 
 
    def auszahlen(self, betrag): 
        """ Mach eine Auszahlung """
        print(f".. Auszahlen   : {self.Kontonummer} {betrag:.1f}")
        self.Kontostand  -= betrag 

    def zeige_konto(self): 
        """ Zeige die Kontodaten am Bildschirm """
        print(".. Konto       :", self.Inhaber)
        print("   Kontonummer :", self.Kontonummer)
        print("   Kontostand  :", self.Kontostand) 


class Girokonto(Konto): 
    """ Giro Konto Klasse """

    def __init__(self, inhaber, kontonummer, kontostand, sollzinsen, habenzinsen): 
        """ Giro Konstruktor, Aufruf bei Instanzierung """                
        self.__Sollzinsen = sollzinsen
        self.__Habenzinsen = habenzinsen
        # initialisiere Konto
        Konto.__init__(self, inhaber, kontonummer, kontostand)

    def ueberweisung(self, ziel, betrag): 
        """ Mach eine Ueberweisung """
        print(f".. Transfer    : {self.Kontonummer} -> {ziel.Kontonummer} {betrag:.1f}")
        self.Kontostand  -= betrag 
        ziel.Kontostand  += betrag 


class Sparkonto(Konto): 
    """ Sparbuch Konto Klasse """
    
    def __init__(self, inhaber, kontonummer, kontostand, zinssatz): 
        """ Spar Konstruktor, Aufruf bei Instanzierung """                
        self.Zinssatz = zinssatz
        # initialisiere des Kontos
        Konto.__init__(self, inhaber, kontonummer, kontostand)

    def zeige_konto(self): 
        """ Zeige die Kontodaten am Bildschirm, ueberschreibt Konto Funktion """
        Konto.zeige_konto(self)
        print("   Zinssatz    :", self.Zinssatz) 
        

if __name__ == '__main__':
    
    print("\nKontobeispiel mit Vererbung")
    
    # Erzeuge zwei Konto-Objekte
    kg = Girokonto("Heinz Meier", 78340, 12000.0, 0.05, 0.01) 
    ks = Sparkonto("Heinz Meier", 78341,  4000.0, 0.03)  

    print("------------------------------")
    
    # Mach was damit ...
    kg.einzahlen(1000)
    ks.einzahlen(2000)    
    kg.auszahlen(300)   
    kg.ueberweisung(ks, 100)    
              
    print("------------------------------")

    # Zeige Kontodaten am Bildschirm    
    kg.zeige_konto()    
    ks.zeige_konto() 
    

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 Übersichtlichkeit halber nicht implementiert.

8.4. Polymorphismus

Polymorphismus (griechisch, „Vielgestaltigkeit“) ist ein Konzept in der Programmierung

  • mit ein und der selben Funktion (z.B. print) bzw. selben Operatoren (+,``-,``*,...)
  • verschiedene Datentypen verwenden zu können.

Hier ein einfaches Beispiel (oop_vektor.py):

class Vektor: 
    """ 2D Vektor Klasse """

    def __init__(self, x, y):              
        self.x = x 
        self.y = y 

    def __add__(self, other): 
        """ Ueberladener '+' Operator """
        return Vektor( self.x+other.x, self.y+other.y )

    def __str__(self): 
        """ Ueberladene print Funktion """
        return f"[{self.x} {self.y}]" 
        

if __name__ == '__main__':
    
    print("\nBeispiel Polymorphismus")
    
    # Erzeuge zwei Vektor Objekte
    v1 = Vektor(3,4)
    v2 = Vektor(1,2) 
    
    # Addiere 2 Vektoren
    vs = v1 + v2 
              
    # Ausgabe des neuen Vektors    
    print('vs =', vs)


    

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 Magic Members welche überladen werden können. Dazu gehören z.B.:

    __init__()      __del__()       __str__()      __iter__()
    __add__()       __sub__()       __mul__()      __gt__()
    

8.5. Ü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:

||\mathbf{v}|| = \sqrt{ \sum_{i=1}^n v_i^2 }

  1. Ü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
    
  2. 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 bekommmt 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.

8.6. 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