Mehrfachvererbung

Einführung

Diamond oder Diamond-Problem Erbt eine abgeleitete Klasse direkt von mehr als einer Basisklasse spricht man in der objektorientierten Programmierung von Mehrfachvererbungen (englisch: multiple inheritance). Ein sequentielles, mehrstufiges Erben wird dagegen nicht als Mehrfachvererbung bezeichnet. Nicht alle objektorientierten Programmiersprachen unterstützen Mehrfachvererbung. So kennt Java1 sie beispielsweise nicht, wohingegen C++ sie unterstützt. Ein Argument gegen die Einbeziehung einer Mehrfachvererbung im Design einer Programmiersprache besteht darin, dass sie das Design von Klassen unnötig kompliziert und undurchsichtig macht. Besonders bekannt sind die Mehrdeutigkeiten die im sogenannten Diamond-Problem beschrieben werden. Wir gehen auf dieses Problem später ein.

Syntaktisch realisiert Python die Mehrfachvererbung wie folgt: Soll eine Klasse von mehreren Basisklassen erben, gibt man die Basisklassen durch Kommata getrennt in die Klammern hinter dem Klassennamen an:

class UnterKlasse(Basis1, Basis2, Basis3, ...):
    pass


Wie bei der einfachen Vererbung können Attribute und Methoden mit gleichen Namen in der Unterklasse und in Basisklassen vorkommen. Die obigen Basisklassen Basis1, Basis2 usw. können natürlich ihrerseits wieder von anderen Basisklassen geerbt haben. Dadurch erhält man eine Vererbungshierarchie, die man sich als einen Baum vorstellen kann, den Vererbungsbaum.

Vererbungsbaum bei Mehrfachvererbung

Beispiel: CalendarClock

Wir wollen die Prinzipien der Mehrfachvererbung an einem Beispiel einführen. Dazu implementieren wir zuerst zwei voneinander unabhängige Klassen: eine Klasse ,,Clock'' und eine Klasse ,,Calendar''. Dann schreiben wir eine Klasse ,,CalendarClock'', die aus einer Uhr und einem Kalender besteht. Dazu lassen wir die Klasse ,,CalendarClock'' von den Klassen ,,Clock'' und ,,Calendar'' erben.

CalendarClock

Die Klasse Clock simuliert das sekundenmäßige Ticken einer Uhr. Eine Instanz dieser Klasse enthält eine Uhrzeit, die wir in den Attributen self.hours, self.minutes und self.seconds speichern. Im Prinzip hätten wir die __init__-Methode und die set-Methode zum Neusetzen einer Uhrzeit wie folgt schreiben können:

    def __init__(self,hours=0, minutes=0, seconds=0):
        self._hours = hours
        self.__minutes = minutes
        self.__seconds = seconds

    def set(self,hours, minutes, seconds=0):
        self._hours = hours
        self.__minutes = minutes
        self.__seconds = seconds

Wir wollten aber gerne noch Plausibilitätsprüfungen für die Eingaben durchführen. Damit wir den dazu nötigen Code nicht gleichzeitig in der __init__-Methode und der set-Methode vorhalten müssen, rufen wir in der __init__-Methode nur die set-Methode auf, die dann die eigentliche Initialisierung inklusive Überprüfungen vornimmt.
Die komplette Klasse ,,Clock'':

""" 
Die Klasse Clock dient der logischen Simulation einer Uhr. 
Die Uhrzeit kann mit einer Methode sekundenweise weiterbewegt 
werden. 
"""

class Clock(object):

    def __init__(self, hours, minutes, seconds):
        """
        Die Parameter hours, minutes, seconds müssen Ganzzahlen 
        sein, und es muss gelten:
        0 <= h < 24
        0 <= m < 60
        0 <= s < 60
        """

        self.set_Clock(hours, minutes, seconds)

    def set_Clock(self, hours, minutes, seconds):
        """
        Die Parameter hours, minutes, seconds müssen Ganzzahlen 
        sein, und es muss gelten:
        0 <= h < 24
        0 <= min < 60
        0 <= sec < 60
        """

        if type(hours) == int and 0 <= hours and hours < 24:
            self._hours = hours
        else:
            raise TypeError("Stunden müssen Ganzzahlen zwischen 0 und 23 sein!")
        if type(minutes) == int and 0 <= minutes and minutes < 60:
            self.__minutes = minutes 
        else:
            raise TypeError("Minuten müssen Ganzzahlen zwischen 0 und 59 sein!")
        if type(seconds) == int and 0 <= seconds and seconds < 60:
            self.__seconds = seconds
        else:
            raise TypeError("Sekunden müssen Ganzzahlen zwischen 0 und 59 sein!")

    def __str__(self):
        """ 
        Diese Methode überlädt die eingebaute Funktion str(),
        d.h. es wird eine Methode zur Verfügung gestellt, 
        um ein Objekt der Klasse Clock in einen String zu wandeln.
        Die Methode __str__ wird auch von der Print-Funktion
        genutzt, um ein Objekt der Klasse Clock auszugeben.
        """

        return "{0:02d}:{1:02d}:{2:02d}".format(self._hours,
                                                self.__minutes,
                                                self.__seconds)

    def tick(self):
        """
        Diese Methode lässt die Uhr 'ticken', d.h. die interne 
        Uhrzeit eines Objektes, d.h. die Stunden-, 
        Minuten- und Sekunden-Attribute werden um eine Sekunde
        weitergerechnet.

        Beispiele:
        >>> x = Clock(12,59,59)
        >>> print(x)
        12:59:59
        >>> x.tick()
        >>> print(x)
        13:00:00
        >>> x.tick()
        >>> print(x)
        13:00:01
        """

        if self.__seconds == 59:
            self.__seconds = 0
            if self.__minutes == 59:
                self.__minutes = 0
                if self._hours == 23:
                    self._hours = 0
                else:
                    self._hours += 1
            else:
                self.__minutes += 1
        else:
            self.__seconds += 1


if __name__ == "__main__":
    x = Clock(23,59,59)
    print(x)
    x.tick()
    print(x)
    y = str(x)
    print(type(y))


Ruft man das Modul clock standalone auf, erhält man folgende Ausgabe:

$ python3 clock.py 
23:59:59
00:00:00
<class 'str'>

Aber was passiert, wenn jemand statt Ganzzahlen Fließkommazahlen oder Strings eintippt? Oder falls jemand eine Minutenzahl eingibt, welche die Zahl 59 übersteigt? Wir wollen diese und andere Fehlerfälle in der folgenden interaktiven Python-Shell-Sitzung testen:

>>> from clock import Clock
>>> x = Clock(7.7,45,17)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "clock.py", line 18, in __init__
    self.set_Clock(h,min,sec)
  File "clock.py", line 32, in set_Clock
    raise TypeError("Stunden müssen Ganzzahlen zwischen 0 und 23 sein!")
TypeError: Stunden müssen Ganzzahlen zwischen 0 und 23 sein!
>>> x = Clock(24,45,17)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "clock.py", line 18, in __init__
    self.set_Clock(h,min,sec)
  File "clock.py", line 32, in set_Clock
    raise TypeError("Stunden müssen Ganzzahlen zwischen 0 und 23 sein!")
TypeError: Stunden müssen Ganzzahlen zwischen 0 und 23 sein!
>>> x = Clock(23,60,17)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "clock.py", line 18, in __init__
    self.set_Clock(h,min,sec)
  File "clock.py", line 36, in set_Clock
    raise TypeError("Minuten müssen Ganzzahlen zwischen 0 und 59 sein!")
TypeError: Minuten müssen Ganzzahlen zwischen 0 und 59 sein!
>>> x = Clock("23","60","17")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "clock.py", line 18, in __init__
    self.set_Clock(h,min,sec)
  File "clock.py", line 32, in set_Clock
    raise TypeError("Stunden müssen Ganzzahlen zwischen 0 und 23 sein!")
TypeError: Stunden müssen Ganzzahlen zwischen 0 und 23 sein!
>>> x = Clock(23,17)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() takes exactly 4 arguments (3 given)
>>> 


Für unsere Klasse ,,CalendarClock'', die eine Uhrzeitfunktion mit Kalenderfunktion kombiniert, benötigen wir noch eine Klasse ,,Calendar''. Während eine Instanz einer Clock-Klasse mittels der Methode ,,tick'' sekundenweise ihren Zustand ändert, geschieht dies bei der ,,Calendar''-Klasse tageweise. Die Methode zum Wechseln oder ,,Weiterblättern'' der Tage nennen wir nicht ,,tick'' sondern ,,advance''. Sie zählt zu einem gegebenen Kalenderdatum einen Tag hinzu. Dabei müssen wir die Anzahl der Tage für die Monate berücksichtigen, also z.B. nach dem 30. April kommt der 1. Mai, und nach dem 31. Januar kommt der 1. Februar. Apropos Februar, hier haben wir ein weiteres Problem, denn wir müssen nun wissen, ob es sich um ein Datum in einem Schaltjahr handelt oder nicht. In unserer Klasse implementieren wir diese Funktionalität unter dem Namen ,,leapyear'' als statische Methode, die True zurückliefert, wenn es sich um ein Schaltjahr handelt und ansonsten False.
Wir berücksichtigen die folgenden Regeln um zu ermitteln, ob es sich um ein Schaltjahr handelt: Für die Anzahl der Monate benutzen wir ein Tupel als Klassenattribut.
Anmerkung: Wir haben uns für den Typ tuple statt list entschieden, da sich ja die Anzahl der Tage in einem Monat nicht ändert, wenn man vom Februar absieht.

""" 
Die Klasse Calendar implementiert einen Kalender. 
Ein Kalenderdatum kann auf ein bestimmtes Datum gesetzt werden
oder kann um einen Tag weitergeschaltet werden.  
"""

class Calendar(object):

    months = (31,28,31,30,31,30,31,31,30,31,30,31)

    @staticmethod
    def leapyear(jahr):
        """ 
        Die Methode leapyear liefert True zurück, wenn jahr
        ein Schaltjahr ist, und False, wenn nicht.
        """

        if jahr % 4 == 0:
            if jahr % 100 == 0:
                if jahr % 400 == 0:
                    schaltjahr = True
                else:
                    schaltjahr = False
            else:
                schaltjahr = True
        else:
            schaltjahr = False

        return schaltjahr


    def __init__(self, d, m, y):
        """
        d, m, y müssen ganze Integer-Werte sein und y muss ein vierstelliger Wert sein
        """

        self.set_Calendar(d,m,y)


    def set_Calendar(self, d, m, y):
        """
        d, m, y müssen ganze Integer-Werte sein und y muss ein vierstelliger Wert sein
        """

        if type(d) == int and type(m) == int and type(y) == int:
            self.__days = d
            self.__months = m
            self.__years = y
        else:
            raise TypeError("d, m, y müssen ganze Zahlen sein!")


    def __str__(self):

        """ 
        Diese Methode überlädt die eingebaute Funktion str(),
        d.h. es wird eine Methode zur Verfügung gestellt, 
        um ein Objekt der Klasse Calendar in einen String zu 
        wandeln.
        Die Methode __str__ wird auch von der Print-Funktion
        genutzt, um ein Objekt der Klasse Calendar auszugeben.

        """

        return "{0:02d}.{1:02d}.{2:4d}".format(self.__days,
                                               self.__months,
                                               self.__years)


    def advance(self):
        """
        setzt den Kalender auf den nächsten Tag
        unter Berücksichtigung von Schaltjahren
        """

        max_days = Calendar.months[self.__months-1]
        if self.__months == 2 and Calendar.leapyear(self.__years):
            max_days += 1
        if self.__days == max_days:
            self.__days= 1
            if self.__months == 12:
                self.__months = 1
                self.__years += 1
            else:
                self.__months += 1
        else:
            self.__days += 1


if __name__ == "__main__":
    x = Calendar(31,12,2012)
    print(x, end=" ")
    x.advance()
    print("nach advance: ", x)
    print("2012 war ein Schaltjahr:")
    x = Calendar(28,2,2012)
    print(x, end=" ")
    x.advance()
    print("nach advance: ", x)
    x = Calendar(28,2,2013)
    print(x, end=" ")
    x.advance()
    print("nach advance: ", x)
    print("1900 war kein Schaltjahr: Zahl durch 100, aber nicht durch 400 teilbar:")
    x = Calendar(28,2,1900)
    print(x, end=" ")
    x.advance()
    print("nach advance: ", x)
    print("2000 war ein Schaltjahr, weil die Zahl durch 400 teilbar ist:")
    x = Calendar(28,2,2000)
    print(x, end=" ")
    x.advance()
    print("nach advance: ", x)


Obiges Skript liefert folgende Ausgabe, wenn man es selbständig startet:

$ python3 calendar.py 
31.12.2012 nach advance:  01.01.2013
2012 war ein Schaltjahr:
28.02.2012 nach advance:  29.02.2012
28.02.2013 nach advance:  01.03.2013
1900 war kein Schaltjahr: Zahl durch 100, aber nicht durch 400 teilbar:
28.02.1900 nach advance:  01.03.1900
2000 war ein Schaltjahr, weil die Zahl durch 400 teilbar ist:
28.02.2000 nach advance:  29.02.2000

In unserem Test haben wir keine Fehlertests eingebaut. Wir sehen aber im Code der Methode set_Calendar, dass wir einen Fehler erheben, wenn einer der Werte für das Datum keine Ganzzahl ist. Wir testen die Fehlerfälle in der interaktiven Python-Shell:


>>> from calendar import Calendar
>>> x = Calendar(31,12,2012)
>>> x = Calendar("31",12,2012)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "calendar.py", line 34, in __init__
    self.set_Calendar(d,m,y)
  File "calendar.py", line 47, in set_Calendar
    raise TypeError("d, m, y müssen ganze Zahlen sein!")
TypeError: d, m, y müssen ganze Zahlen sein!
>>> x = Calendar(12,2012)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() takes exactly 4 arguments (3 given)
>>> 

Nun geht es darum, eine Klasse CalendarClock zu implementieren, die eine Uhr mit integriertem Kalender repräsentiert. Die Klasse CalendarClock erbt von der Klasse Clock und der Klasse Calendar. Die Methode tick von Clock wird von CalendarClock überschrieben. Die neue tick-Methode muss jedoch auf die tick-Methode von Clock zugreifen. Man kann diese jedoch nicht mit self.tick() aufrufen, da dies die tick-Methode von CalendarClock repräsentiert. Der Aufruf erfolgt deshalb mit Clock.tick(self). Die advance-Methode von Calendar kann jedoch direkt mit self.advance() aufgerufen werden. Natürlich wäre der Aufruf auch mit Calendar.advance(self) möglich und richtig.

""" 
Modul, das die Klasse CalendarClock implementiert.
"""

from clock import Clock
from calendar import Calendar


class CalendarClock(Clock, Calendar):
    """ 
        Die Klasse CalendarClock implementiert eine Uhr mit integrierter
        Kalenderfunktion.  Die Klasse erbt sowohl von der Klasse Clock 
        als auch von der Klasse Calendar.
    """

    def __init__(self,day, month, year, hour, minute, second):
        """
        Zur Initialisierung der Uhrzeit wird der Konstruktor der Clock-Klasse
        aufgerufen. Zur Initialisierung des Kalenders wird der Konstruktor der 
        Calendar-Klasse aufgerufen.

        CalendarClock enthält dann die vereinigten Attribute der Clock- und 
        Calendar-Klasse:
        self.day, self.month, self.year, self.hour, self.minute, self.second
        """

        Clock.__init__(self,hour, minute, second)
        Calendar.__init__(self,day, month, year)


    def tick(self):
        """
        Die Position der Uhr wird um eine Sekunde weiterbewegt,
        der Kalender wird, falls Mitternacht überschritten wird, 
        um einen Tag weiterbewegt.
        """

        previous_hour = self._hours
        Clock.tick(self)
        if (self._hours < previous_hour): 
            self.advance()

    def __str__(self):
        """
        Erzeugt die Stringdarstellung eines CalendarClock-Objekts
        """
        return Calendar.__str__(self) + ", " + Clock.__str__(self)


if __name__ == "__main__":
    x = CalendarClock(31,12,2013,23,59,59)
    print("One tick from ",x, end=" ")
    x.tick()
    print("to ", x)

    x = CalendarClock(28,2,1900,23,59,59)
    print("One tick from ",x, end=" ")
    x.tick()
    print("to ", x)

    x = CalendarClock(28,2,2000,23,59,59)
    print("One tick from ",x, end=" ")
    x.tick()
    print("to ", x)

    x = CalendarClock(7,2,2013,13,55,40)
    print("One tick from ",x, end=" ")
    x.tick()
    print("to ", x)

Ruft man obiges Modul standalone auf, erhält man folgende Ausgabe, die gleichzeitig auch die Arbeitsweise nochmals ein wenig erklärt:

$ python3 calendar_clock.py 
One tick from  31.12.2013 23:59:59 to  01.01.2014 00:00:00
One tick from  28.02.1900 23:59:59 to  01.03.1900 00:00:00
One tick from  28.02.2000 23:59:59 to  29.02.2000 00:00:00
One tick from  07.02.2013 13:55:40 to  07.02.2013 13:55:41


Diamond-Problem oder ,,deadly diamond of death''

Diamond-Problem Bei dem Diamond-Problem (englisch: diamond problem, auch als ,,deadly diamond of death'' bekannt) handelt es sich um ein Mehrdeutigkeitsproblem, was bei Mehrfachvererbung in der objektorientierten Programmierung entstehen kann. Es kann auftreten, wenn eine Klasse D auf zwei verschiedenen Vererbungspfaden über eine Klasse B und eine Klasse C von der gleichen Basisklasse A abstammt. Das Problem tritt auf, falls in D eine Methode, sagen wir ,,m'', aufgerufen wird, für die gilt: Die Frage, die sich dann stellt: Von welcher Klasse wird m vererbt?
In Python hängt es zunächst einmal von der Reihenfolge der Klassen bei der Definition von D ab:

Wir können die verschiedenen Möglichkeiten mit einfachen Klassendefinitionen testen. Zuerst wollen wir uns den Fall anschauen, dass sowohl B als auch C die Methode m überschreiben:

class A:
    def m(self):
        print("m of A called")

class B(A):
    def m(self):
        print("m of B called")
    
class C(A):
    def m(self):
        print("m of C called")

class D(B,C):
    pass

Ruft man x.m() für eine Instanz x der Klasse D auf, erhalten wir die Ausgabe ,,m of B called''. Vertauschen wir nun die Reihenfolge der Klassen B und C im Klassenheader von D, also ,,class D(C,B):'', dann erhalten wir die Ausgabe ,,m of C called''.

Von besonderem Interesse ist jedoch der Fall, wenn m nur entweder in B oder in C überschrieben wird:

class A:
    def m(self):
        print("m of A called")

class B(A):
    pass
    
class C(A):
    def m(self):
        print("m of C called")

class D(B,C):
    pass

x = D()
x.m()

Im obigen Fall sind prinzipiell beide Möglichkeiten denkbar, d.h. ,,m von C'' oder ,,m von A'' wird aufgerufen.

Ruft man dieses Programm mit einer 2er-Version von Python auf, erkennt man, dass m von A aufgerufen wird:

$ python diamond_problem.py 
m of A called

Vor der Einführung der sogenannten ,,New Style''-Klassen in Python2 erfolgte eine Tiefensuche (englisch: depth-first search, DFS) im Vererbungsbaum, d.h. man geht solange in der Suche nach der Methode m nach oben, bis es nicht mehr weiter nach oben geht, dann sucht man von links nach rechts weiter.

Rufen wir das obige Skript nun unter Python3 auf, wird -- anders als bei Python2 -- die Methode m der Klasse C aufgerufen:

$ python3 diamond_problem.py 
m of C called

Ändert man die Vererbungsreihenfolge bei der Definition von D, also "class D(C,B):" statt "class D(B,C):", dann wird sowohl in Python3 als auch in Python2 die Methode m von C für eine Instanz aus D benutzt.

Sicherlich fragen Sie sich noch, woher der Name ,,Diamond-Problem'' kommt: Zeichnet man die Vererbungsbeziehungen zwischen den Klassen in einem Diagramm, so sieht das Ergebnis wie eine Raute aus, was im Englischen auch als ,,diamond'' bezeichnet wird.

super and MRO

Wir haben eben gesehen, wie Python das Diamond-Problem löst, d.h. in welcher Reihenfolge die Basisklassen durchsucht werden. Diese Reihenfolge wird durch die sogenannte ,,Method Resolution Order'', kurz MRO, festgelegt.2

Wir erweitern nun unser voriges Beispiel dahingehend, dass nun jede Klasse eine eigene Methode m erhält:
class A:
    def m(self):
        print("m of A called")

class B(A):
    def m(self):
        print("m of B called")
    
class C(A):
    def m(self):
        print("m of C called")

class D(B,C):
    def m(self):
        print("m of D called")


Wenden wir auf eine Instanz x der Klasse D die Methode m an, so wird nun nur noch der Code der Methode D aufgerufen. Möchte man explizit die Methoden m der Klassen A, B und C aufrufen, so funktioniert dies, wie in der folgenden interaktiven Python-Shell:

>>> from super1 import A,B,C,D
>>> x = D()
>>> B.m(x)
m of B called
>>> C.m(x)
m of C called
>>> A.m(x)
m of A called

Wenn unsere Methode m von D automatisch immer auch noch die m-Methoden der anderen Klassen aufrufen soll, können wir dies einfach in den Body unserer Methode entsprechend aufnehmen:

class D(B,C):
    def m(self):
        print("m of D called")
        B.m(self)
        C.m(self)
        A.m(self)

Das funktioniert, wie wir uns im Folgenden vergewissern können:

>>> from mro import D
>>> x = D()
>>> x.m()
m of D called
m of B called
m of C called
m of A called

Interessant oder besser gesagt problematisch wird es, wenn auch die Methoden m von B und C jeweils A aufrufen müssen bzw. sollen. In diesem Fall müssen wir den Aufruf A.m(self) aus der Methode m von D herausnehmen.

class A:
    def m(self):
        print("m of A called")

class B(A):
    def m(self):
        print("m of B called")
        A.m(self)
    
class C(A):
    def m(self):
        print("m of C called")
        A.m(self)

class D(B,C):
    def m(self):
        print("m of D called")
        B.m(self)
        C.m(self)

Wir haben aber nun das Problem, dass unsere Methode m von A zweimal aufgerufen wird, d.h. einmal über B und einmal über C:

>>> from super3 import D
>>> x = D()
>>> x.m()
m of D called
m of B called
m of A called
m of C called
m of A called

Eine Lösungsmöglichkeit -- allerdings eine nicht pythonische -- besteht darin die Methoden m in B und C in zwei Teile zu splitten, und zwar in den speziellen Code, der speziell in der Klasse ergänzt wird (_m)und die allgemeine Methode m, die _m und m von A aufruft. In der Methode m von D rufen wir dann die speziellen Methoden von B und C auf. Die Methode m von A müssen wir nun wieder in m von D explizit aufrufen. Insgesamt haben wir das Problem -- auf Kosten eines aufwendigen und schwerverständlichen Designs -- gelöst:

class A:
    def m(self):
        print("m of A called")

class B(A):
    def _m(self):
        print("m of B called")
    def m(self):
        self._m()
        A.m(self)
    
class C(A):
    def _m(self):
        print("m of C called")
    def m(self):
        self._m()
        A.m(self)

class D(B,C):
    def m(self):
        print("m of D called")
        B._m(self)
        C._m(self)
        A.m(self)

Wir können sehen, dass es so funktioniert, wie wir es uns gewünscht hatten:

>>> from super4 import D
>>> x = D()
>>> x.m()
m of D called
m of B called
m of C called
m of A called


Der optimale oder der ,,super'' pythonische Weg besteht in der Benutzung der super-Funktion von Python:

class A:
    def m(self):
        print("m of A called")

class B(A):
    def m(self):
        print("m of B called")
        super().m()
    
class C(A):
    def m(self):
        print("m of C called")
        super().m()

class D(B,C):
    def m(self):
        print("m of D called")
        super().m()

Wir erhalten wieder die gewünschte Ausgabe, aber diesmal mit einem perfekten Design:

>>> from super5 import D
>>> x = D()
>>> x.m()
m of D called
m of B called
m of C called
m of A called

Die super-Funktion wird sehr häufig bei der Initialisierung von Instanzen, also bei der __init__-Methode verwendet. Im Folgenden sehen wir ein Beispiel:

class A:
    def __init__(self):
        print("A.__init__")

class B(A):
    def __init__(self):
        print("B.__init__")
        super().__init__()
    
class C(A):
    def __init__(self):
        print("C.__init__")
        super().__init__()


class D(B,C):
    def __init__(self):
        print("D.__init__")
        super().__init__()

Die Arbeitsweise verdeutlichen wir in der folgenden Benutzung der Klassen:

>>> from super_init import A,B,C,D
>>> d = D()
D.__init__
B.__init__
C.__init__
A.__init__
>>> c = C()
C.__init__
A.__init__
>>> b = B()
B.__init__
A.__init__
>>> a = A()
A.__init__

Es stellt sich nun die Frage, wie die Funktion super entscheidet, welche Klasse sie benutzt? Dazu benutzt sie die eingangs erwähnte Methode Resolution Order (MRO), die auf dem ,,C3 superclass linearization''-Algorithmus aufbaut. Man spricht auch von einer Linearisierung, weil aus der Baumstruktur eine lineare Reihenfolge wird, also eine (geordnete) Liste. Um diese Liste zu generieren, bietet Python die mro-Methode:

>>> from super_init import A,B,C,D
>>> D.mro()
[<class 'super_init.D'>, <class 'super_init.B'>, <class 'super_init.C'>, <class 'super_init.A'>, <class 'object'>]
>>> B.mro()
[<class 'super_init.B'>, <class 'super_init.A'>, <class 'object'>]
>>> A.mro()
[<class 'super_init.A'>, <class 'object'>]


Polymorphie

Ein wesentliches Konzept der objektorientierten Programmierung stellt die Polymorphie dar. Der Wort Polymorphie oder Polymorphismus stammt aus dem griechischen und bedeutet Vielgestaltigkeit. Polymorphismus bezeichnet bei Methoden die Möglichkeit, dass man bei gleichem Namen Methoden mit verschiedenen Parametern aufrufen kann. Man spricht dann vom Überladen von Methoden. In Python kann man auch Operatoren und Standardfunktionen überladen, worauf wir im nächsten Abschnitt eingehen.

Methoden und Funktionen in Python haben bereits eine implizite Polymorphie wegen des dynamischen Typkonzepts von Python.

Betrachten wir die folgende Python-Funktion:

def f(x, y):
    print("values: ", x, y)

f(42,43)
f(42, 43.7) 
f(42.3,43)
f(42.0, 43.9)


Die Funktion f rufen wir mit verschiedenen Typ-Paarungen auf. Beim ersten Aufruf mit (int, int), dann mit (int, float), dann (float, int) und im vierten Aufruf mit (float, float). In getypten Programmiersprachen wie C++ müssten wir entsprechend dieser Typ-Paarungen f in vier Varianten definieren. Ein C++-Programm, das dem obigen Python-Programm entspricht, könnte wie folgt aussehen:

#include <iostream>
using namespace std;

void f(int x, int y ) {
    cout << "values: " << x << ", " << x << endl;
}

void f(int x, double y ) {
    cout << "values: " << x << ", " << x << endl;
}

void f(double x, int y ) {
    cout << "values: " << x << ", " << x << endl;
}

void f(double x, double y ) {
    cout << "values: " << x << ", " << x << endl;
}


int main()
{
    f(42, 43); 
    f(42, 43.7); 
    f(42.3,43);
    f(42.0, 43.9); 
}

Bei der Funktion f unseres Python-Programms ist der Polymorphismus nicht nur auf Integer- und Float-Werte beschränkt. Wir können die Funktion auf alle Typen und Klassen anwenden, die sich mittels print drucken lassen:

>>> def f(x,y):
...     print("values: ", x, y)
... 
>>> f([3,5,6],(3,5))
values:  [3, 5, 6] (3, 5)
>>> f("A String", ("A tuple", "with Strings"))
values:  A String ('A tuple', 'with Strings')
>>> f({2,3,9}, {"a"=3.4,"b"=7.8, "c"=9.04})
  File "<stdin>", line 1
    f({2,3,9}, {"a"=3.4,"b"=7.8, "c"=9.04})
                   ^
SyntaxError: invalid syntax
>>> f({2,3,9}, {"a":3.4,"b":7.8, "c":9.04})
values:  {9, 2, 3} {'a': 3.4, 'c': 9.04, 'b': 7.8}
>>> 

Fußnoten

1
Man hat das Problem in Java mit Mehrfach-Schnittstellenvererbung umgangen. Eine Klasse kann in Java von beliebig vielen Schnittstellen erben.

2
Python benutzt ab Version 2.3 den ,,C3 superclass linearization''-Algorithmus um die MRO festzulegen.