(c) 2023 Technische Hochschule Augsburg - Fakultät für Informatik - Prof.Dr.Nik Klever - Impressum
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:
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.
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.
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:
%%Mooc Video
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()
print(isinstance(instanzAbgeleiteteKlasse, BasisKlasse))
True
print(isinstance(instanzBasisKlasse, AbgeleiteteKlasse))
False
print(issubclass(AbgeleiteteKlasse, BasisKlasse))
True
print(issubclass(BasisKlasse, AbgeleiteteKlasse))
False
%%Mooc StringAssessment
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)
%%Mooc Video
%%Mooc StringAssessment
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.
%%Mooc Video
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.
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
Die Methode super(typ, object) gibt ein Objekt zurück, das Methodenaufrufe an eine übergeordnete oder geschwisterliche Klasse des Typs typ delegiert:
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.
%%Mooc StringAssessment
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=" ")
%%Mooc WebReference
https://docs.python.org/3/tutorial/classes.html#inheritance
Hinweis: Vererbung und Mehrfachvererbung von Klassen
%%Mooc WebReference
https://docs.python.org/3/library/datetime.html#datetime.date.__str__
Hinweis: Die Standard __str__ Methode für Date-Objekte liefert isoformat zurück
%%Mooc WebReference
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
%%Mooc WebReference
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