Vererbung (Inheritance)

Grundlegendes

Die Definition einer Klasse ohne der Unterstützung von Vererbung ist für eine Programmiersprache heutzutage nicht denkbar. Vererbung bedeutet hierbei, dass die abgeleitete neue Klasse AbgeleiteteKlasse eben alle Klassenvariablen und Klassenmethoden von der vererbenden Klasse BasisKlasse erbt sodass diese Variablen und Methoden auch in der abgeleiteten Klasse zur Verfügung stehen. Die Syntax für eine abgeleitete Klassendefinition sieht wie folgt aus:

class AbgeleiteteKlasse(BasisKlasse):
    <Ausdruck-1>
    .
    .
    .
    <Ausdruck-N>

Der Name BasisKlasse muss in dem Gültigkeitsbereich definiert sein bzw. in diesen Gültigkeitsbereich importiert worden sein, in dem die abgeleitete Klassendefinition definiert wird, wie z.B. in folgendem Code:

In [2]:
class BasisKlasse:
    def methodeVonBasisKlasse(self, arg):
        print("BasisKlasse.methodeVonBasisKlasse: arg={}".format(arg))

class AbgeleiteteKlasse(BasisKlasse):
    def methodeVonAbgeleiteteKlasse(self, arg):
        print("AbgeleiteteKlasse.methodeVonAbgeleiteteKlasse: arg={}".format(arg))

arg = "Argument arg"
instanzAbgeleiteteKlasse = AbgeleiteteKlasse()
instanzAbgeleiteteKlasse.methodeVonAbgeleiteteKlasse(arg)
instanzAbgeleiteteKlasse.methodeVonBasisKlasse(arg)
AbgeleiteteKlasse.methodeVonAbgeleiteteKlasse: arg=Argument arg
BasisKlasse.methodeVonBasisKlasse: arg=Argument arg

Die letzte Zeile in dem obigen Code zeigt, dass - obwohl die Klasse AbgeleiteteKlasse keine Methode methodeVonBasisKlasse selbst besitzt - diese Methode aufgerufen werden kann, da sie von der übergeordneten BasisKlasse vererbt wird.

Anstelle eines einfachen Klassennamens für die vererbende Klasse in der Definition einer Klasse sind auch andere Ausdrücke erlaubt, hinter denen letztendlich ein Klassenname steckt. Dies kann z.B. nützlich sein, wenn die Basisklasse in einem anderen Modul (einer anderen Bibliothek) definiert ist:

class AbgeleiteteKlasse(modulname.BasisKlasse)

Eine abgeleitete Klassendefinition wird genauso verwendet wie eine Basisklasse. Wenn das Klassenobjekt definiert wird, wird damit auch die Basisklasse ansprechbar. Dies wird dann zum Auflösen von Attributreferenzen verwendet: Wenn ein angefragtes Attribut nicht in der Klasse gefunden wird, wird in der Basisklasse danach gesucht. Diese Regel wird rekursiv angewendet, d.h. wenn die Basisklasse selbst von einer anderen Klasse abgeleitet ist, wird auch diese durchsucht, usw.

Die Instanziierung von abgeleiteten Klassen erfolgt ebenfalls analog wie bei der Basisklasse: AbgeleiteteKlasse() erstellt eine neue Instanz der Klasse. Methodenreferenzen werden dann wie Attributreferenzen aufgelöst: das entsprechende Klassenattribut wird zuerst in der abgeleiteten Klasse gesucht, danach wird die Basisklasse durchsucht und rekursiv die weiteren Basisklassen bis die Methodenreferenz gefunden wurde und auch ein Funktionsobjekt ist.

Abgeleitete Klassen können Methoden ihrer Basisklassen überschreiben. Da Methoden keine speziellen Berechtigungen beim Aufrufen anderer Methoden desselben Objekts haben, kann eine Methode einer Basisklasse, die eine andere Methode aufruft, die in derselben Basisklasse definiert ist, statt dieser Methode in der Basisklasse die Methode einer abgeleiteten Klasse aufrufen, welche die Methode der Basisklasse überschrieben hat.

In [3]:
class BasisKlasse:
    def methode(self, arg):
        print("BasisKlasse.methode: arg={}".format(arg))

class AbgeleiteteKlasse(BasisKlasse):
    def methode(self, arg):
        print("AbgeleiteteKlasse.methode: arg={}".format(arg))

arg = "Argument arg"
instanzBasisKlasse = BasisKlasse()
instanzBasisKlasse.methode(arg)
instanzAbgeleiteteKlasse = AbgeleiteteKlasse()
instanzAbgeleiteteKlasse.methode(arg)
BasisKlasse.methode: arg=Argument arg
AbgeleiteteKlasse.methode: arg=Argument arg

Eine Methode in einer abgeleiteten Klasse, die eine Basisklassenmethode überschreibt, kann diese Basisklassenmethode nicht nur einfach ersetzen sondern auch erweitern. Hierzu gibt es eine einfache Möglichkeit, die Basisklassenmethode direkt aufzurufen: einfach BasisKlasse.methodenname(self, arguments) aufrufen.

In [4]:
class BasisKlasse:
    def methode(self, arg):
        print("BasisKlasse.methode: arg={}".format(arg))

class AbgeleiteteKlasse(BasisKlasse):
    def methode(self, arg):
        print("AbgeleiteteKlasse.methode: arg={}".format(arg))
        
    def basisMethode(self, arg):
        BasisKlasse.methode(self, arg)
        

arg = "Argument arg"
instanzBasisKlasse = BasisKlasse()
instanzBasisKlasse.methode(arg)
instanzAbgeleiteteKlasse = AbgeleiteteKlasse()
instanzAbgeleiteteKlasse.methode(arg)
instanzAbgeleiteteKlasse.basisMethode(arg)
BasisKlasse.methode: arg=Argument arg
AbgeleiteteKlasse.methode: arg=Argument arg
BasisKlasse.methode: arg=Argument arg

Python hat zwei built-in Funktionen, die für Klassen und deren Objekte nützlich sind:

  • isinstance() wird verwendet, um den Typ einer Instanz zu überprüfen: isinstance(obj, int) ist nur wahr, wenn obj.__class__ == int oder eine von int abgeleitete Klasse ist.
  • issubclass() wird verwendet, um die Vererbung der Klasse zu überprüfen: issubclass(bool, int) ist True, da bool eine Unterklasse von int ist. Allerdings ist issubclass(float, int) falsch, da float keine Unterklasse von int ist.
In [5]:
%%Mooc Video
Out[5]:
In [6]:
class BasisKlasse:
    def methode(self, arg):
        print("BasisKlasse.methode: arg={}".format(arg))

class AbgeleiteteKlasse(BasisKlasse):
    def methode(self, arg):
        print("AbgeleiteteKlasse.methode: arg={}".format(arg))
        
    def basisMethode(self, arg):
        BasisKlasse.methode(self, arg)
        

instanzBasisKlasse = BasisKlasse()
instanzAbgeleiteteKlasse = AbgeleiteteKlasse()
In [7]:
print(isinstance(instanzAbgeleiteteKlasse, BasisKlasse))
True
In [8]:
print(isinstance(instanzBasisKlasse, AbgeleiteteKlasse))
False
In [9]:
print(issubclass(AbgeleiteteKlasse, BasisKlasse))
True
In [10]:
print(issubclass(BasisKlasse, AbgeleiteteKlasse))
False
In [11]:
%%Mooc StringAssessment
Out[11]:

isinstance - Überprüfung von Argumenten

Damit die Argumente einer Funktion als unterschiedliche Objekte angegeben werden können, kann die Funktion isinstance verwendet werden.

Der folgende Code zeigt ein Beispiel hierzu um ein Datum sowohl als Zeichenkette, als 3-teiliges Tupel oder auch als Date-Objekt übergeben zu können:


import datetime

class Person():
    def __init__(self, nachname, vorname, geburtsdatum):
        self.Nachname = nachname
        self.Vorname = vorname
        if isinstance(geburtsdatum, datetime.date):
            self.Geburtsdatum = geburtsdatum
        elif isinstance(geburtsdatum, tuple):
            self.Geburtsdatum = datetime.date(*geburtsdatum)
        elif isinstance(geburtsdatum, str):
            self.Geburtsdatum = datetime.datetime.strptime(geburtsdatum,"%Y-%m-%d").date()

michi = Person("Mustermann","Michael","1990-5-13")
hans = Person("Mustermann","Hans",(1993,10,1))
erika = Person("Musterfrau","Erika",datetime.date(1995,8,20))
print(michi.Geburtsdatum,hans.Geburtsdatum,erika.Geburtsdatum)

Geben sie das Ergebnis der Print-Funktion an.



In [12]:
%%Mooc Video
Out[12]:
In [13]:
%%Mooc StringAssessment
Out[13]:

Vererbung aus einer Klasse der Standardbibliothek

Damit das obige Beispiel das Geburtsdatum immer im lokalen Länderformat ausgegeben wird, kann eine von der Klasse datetime.date abgeleitete Klasse localDate mit einer einzigen Methode __str__ eingeführt werden, welche die Methode __str__ der Basisklasse datetime.date überschreibt. Alle anderen Methoden erbt die neue Klasse von der Basisklasse datetime.date. Siehe hierzu auch die weitergehende Literatur.

Der folgende Code zeigt das Beispiel hierzu um alle Geburtsdaten im lokalen Länderformat (mittels des Setzens des lokalen Länderformats über locale.setlocale) auszugeben:


import datetime
import locale
locale.setlocale(locale.LC_ALL, locale.getlocale())

class localDate(datetime.date):
    def __str__(self):
        return self.strftime("%x")
    
class Person():
    def __init__(self, nachname, vorname, geburtsdatum):
        self.Nachname = nachname
        self.Vorname = vorname
        if isinstance(geburtsdatum, localDate):
            self.Geburtsdatum = geburtsdatum
        elif isinstance(geburtsdatum, tuple):
            self.Geburtsdatum = localDate(*geburtsdatum)
        elif isinstance(geburtsdatum, str):
            date = datetime.datetime.strptime(geburtsdatum,"%x")
            self.Geburtsdatum = localDate(date.year,date.month,date.day)
    
michi = Person("Mustermann","Michael","13.5.1990")
hans = Person("Mustermann","Hans",(1993,10,1))
erika = Person("Musterfrau","Erika",localDate(1995,8,20))
print(michi.Geburtsdatum,hans.Geburtsdatum,erika.Geburtsdatum)

Einzig die Berechnung des Geburtsdatums aus einer Zeichenkette muss in zwei Zeilen erfolgen, da die notwendige Methode strptime nicht in der Klasse datetime.date sondern nur in datetime.datetime zur Verfügung steht.

Geben sie das Ergebnis der Print-Funktion an.



In [14]:
%%Mooc Video
Out[14]:

Mehrfachvererbung (Multiple Inheritance)

Grundlegendes

Python unterstützt auch eine Form der Mehrfachvererbung. Eine Klassendefinition mit mehreren Basisklassen sieht folgendermaßen aus:

class AbgeleiteteKlasse(BasisKlasse1, BasisKlasse2, BasisKlasse3):
    <Ausdruck-1>
    .
    .
    .
    <Ausdruck-N>

Für die meisten Fälle, insbesondere die einfachsten kann man davon ausgehen, dass die Suche nach Attributen, die von einer übergeordneten Klasse geerbt wurden, zuerst in die Tiefe (also BasisKlasse1 und rekursiv deren Basisklassen), anschliessend von links nach rechts (also anschliessend BasisKlasse2 und rekursiv deren Basisklassen und dann erst BasisKlasse3 und rekursiv deren Basisklassen) durchgeführt wird. Dabei wird nicht zweimal in derselben Klasse durchsucht, wenn es eine Überlappung in der Hierarchie gibt. Wenn also ein Attribut nicht in AbgeleiteteKlasse gefunden wird, wird es in BasisKlasse1 gesucht, dann (rekursiv) in den Basisklassen von BasisKlasse1, und wenn es dort nicht gefunden wird, wird es in BasisKlasse2 gesucht und so weiter.

Tatsächlich ist es etwas komplexer als das oben genannte Vorgehen; die Reihenfolge für die Auflösung von Methoden ändert sich nämlich dynamisch, um kooperative Aufrufe der Methode super() zu unterstützen. Dieser Ansatz wird in einigen anderen Multivererbungssprachen call-next-Methode genannt und ist mächtiger als der Aufruf von super, der in Einfachvererbungssprachen verwendet wird.

In [16]:
class BasisKlasse1:
    def methodeVonBasisKlasse1(self, arg):
        print("BasisKlasse1.methodeVonBasisKlasse1: arg={}".format(arg))

class BasisKlasse2:
    def methodeVonBasisKlasse2(self, arg):
        print("BasisKlasse2.methodeVonBasisKlasse2: arg={}".format(arg))

class AbgeleiteteKlasse(BasisKlasse1, BasisKlasse2):
    def methodeVonAbgeleiteteKlasse(self, arg):
        print("AbgeleiteteKlasse.methodeVonAbgeleiteteKlasse: arg={}".format(arg))

arg = "Argument arg"
instanzAbgeleiteteKlasse = AbgeleiteteKlasse()
instanzAbgeleiteteKlasse.methodeVonAbgeleiteteKlasse(arg)
instanzAbgeleiteteKlasse.methodeVonBasisKlasse1(arg)
instanzAbgeleiteteKlasse.methodeVonBasisKlasse2(arg)
AbgeleiteteKlasse.methodeVonAbgeleiteteKlasse: arg=Argument arg
BasisKlasse1.methodeVonBasisKlasse1: arg=Argument arg
BasisKlasse2.methodeVonBasisKlasse2: arg=Argument arg

Methode super()

Die Methode super(typ, object) gibt ein Objekt zurück, das Methodenaufrufe an eine übergeordnete oder geschwisterliche Klasse des Typs typ delegiert:

In [17]:
class B:
    def methode(self, *args):
        return "B.methode: args={}".format(*args)

class C(B):
    def methode(self, *args):
        ergebnisVonBasisklasse = super(C, self).methode(*args)
        return "C.methode: ergebnisVonBasisklasse='{}'".format(ergebnisVonBasisklasse)

arg = ["Argument1","Argument2"]
instanzC = C()
print(instanzC.methode(arg))
C.methode: ergebnisVonBasisklasse='B.methode: args=['Argument1', 'Argument2']'

In dem obigen Beispiel wird in dem Aufruf instanzC.methode(arg) das Argument arg an die Methode methode der von B abgeleiteten Klasse C übergeben, die Methode methode selbst ruft dann die mit super(C, self).methode(arg) die Methode methode der übergeordneten Klasse B auf.

Eine dynamische Reihenfolge ist notwendig, da alle Fälle von mehrfacher Vererbung eine oder mehrere rautenförmige Vererbungsbeziehungen aufweisen können (d.h. auf mindestens eine der übergeordneten Klassen kann über mehrere Pfade von der untersten Klasse aus zugegriffen werden). Zum Beispiel erben alle Klassen von der Klasse Object, was bedeutet, dass jeder Fall einer Mehrfachvererbung mehr als einen Pfad aufweist, um die Klasse Object zu erreichen. Um zu verhindern dass auf die Basisklassen mehr als einmal zugegriffen wird, linearisiert der dynamische Algorithmus die Suchreihenfolge in einer Weise, die die in jeder Klasse angegebene Links-Rechts-Reihenfolge zwar beibehält, die jedes Elternteil aber nur einmal aufruft und dieser Algorithmus monoton ist (d.h. dass eine Klasse als Unterklasse auftreten kann ohne die Reihenfolge der Elternklassen zu beeinflussen). Alles zusammengenommen, machen diese Eigenschaften es möglich, zuverlässige und erweiterbare Klassen mit Mehrfachvererbung zu entwerfen.

In [18]:
%%Mooc StringAssessment
Out[18]:

Mehrfachvererbung

Der folgende Code zeigt ein Beispiel der Mehrfachvererbung sowohl mit der Überschreibung von Methoden mit der Funktion methode als auch mit der Vererbung von Funktionen aus nur einer Basisklasse mit der Funktion methodeC:


class A:
    def methode(self):
        return "A.methode"
    def methodeA(self):
        return "A.methodeA"
        
class B:
    def methode(self):
        return "B.methode"
    def methodeB(self):
        return "B.methodeB"

class C(B,A):
    def __init__(self,superClass="C"):
        self.superClass = superClass
    def methode(self):
        if self.superClass == "A":
            return A.methode(self)
        elif self.superClass == "B":
            return super(C, self).methode()
        else:
            return "C.methode"
    def methodeC(self,superClass="C"):
        if self.superClass == "A":
            return self.methodeA()
        elif self.superClass == "B":
            return self.methodeB()
        else:
            return "C.methodeC"

for c in ["A","B","C"]:
    instanz = C(c)
    print(instanz.methode(),end=" ")
    print(instanz.methodeC(c),end=" ")

Geben sie das Ergebnis der Print-Funktion an.



Weitere Literatur

In [19]:
%%Mooc WebReference

Inheritance

https://docs.python.org/3/tutorial/classes.html#inheritance

Hinweis: Vererbung und Mehrfachvererbung von Klassen

In [20]:
%%Mooc WebReference

Date.__str__

https://docs.python.org/3/library/datetime.html#datetime.date.__str__

Hinweis: Die Standard __str__ Methode für Date-Objekte liefert isoformat zurück

In [21]:
%%Mooc WebReference

strftime and strptime Behavior

https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior

Hinweis: Die Beschreibung und Dokumentation der Syntax zur Formatierung von strftime und strptime

In [22]:
%%Mooc WebReference

The Python 2.3 Method Resolution Order

https://www.python.org/download/releases/2.3/mro/

Hinweis: Genaueres zum Algorithmus um die Reihenfolge der Auflösung von Methoden in einer Klassenhierarchie mit Mehrfachvererbung zu bestimmen