Mehrfachvererbung
Einführung
Im vorherigen Kapitel unseres Tutorials haben wir uns mit Vererbung oder spezifischer "Einzelvererbung" befasst. Wie wir gesehen haben, erbt eine Klasse in diesem Fall von einer Klasse. Die Mehrfachvererbung hingegen ist eine Funktion, mit der eine Klasse Attribute und Methoden von mehr als einer übergeordneten Klasse erben kann. Die Kritiker weisen darauf hin, dass Mehrfachvererbung in Situationen wie dem Diamantenproblem mit einem hohen Maß an Komplexität und Mehrdeutigkeit einhergeht. Wir werden dieses Problem später in diesem Kapitel ansprechen.
Das weit verbreitete Vorurteil, dass Mehrfachvererbung etwas "Gefährliches" oder "Schlechtes" ist, wird hauptsächlich durch Programmiersprachen mit schlecht implementierten Mehrfachvererbungsmechanismen und vor allem durch deren missbräuchliche Verwendung genährt. Java unterstützt nicht einmal Mehrfachvererbung, während C ++ dies unterstützt. Python hat einen ausgeklügelten und gut konzipierten Ansatz für die Mehrfachvererbung.
Eine Klassendefinition, bei der eine untergeordnete Klasse SubClassName von den übergeordneten Klassen BaseClass1, BaseClass2, BaseClass3 usw. erbt, sieht folgendermaßen aus:
class SubclassName(BaseClass1, BaseClass2, BaseClass3, ...): pass
Es ist klar, dass alle Oberklassen BaseClass1, BaseClass2, BaseClass3, ... auch von anderen Oberklassen erben können. Was wir bekommen, ist ein Vererbungsbaum.
Beispiel: CalendarClock
Wir wollen die Prinzipien der Mehrfachvererbung anhand eines Beispiels vorstellen. Zu diesem Zweck implementieren wir unabhängige Klassen: eine "Clock" - und eine "Calendar" -Klasse. Danach werden wir eine Klasse "CalendarClock" einführen, die, wie der Name schon sagt, eine Kombination aus "Clock" und "Calendar" ist. CalendarClock erbt sowohl von "Clock" als auch von "Calendar".
Die Klasse Clock simuliert das Tick-Tack einer Uhr. Eine Instanz dieser Klasse enthält die Zeit, die in den Attributen self.hours, self.minutes und self.seconds gespeichert ist. Grundsätzlich hätten wir die __init__
Methode und die set Methode 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 haben uns gegen diese Implementierung entschieden, weil wir der Set-Methode zusätzlichen Code zur Überprüfung der Plausibilität der Zeitdaten hinzugefügt haben. Wir rufen die set-Methode auch von der __init__
-Methode auf, weil wir redundanten Code umgehen wollen.
Die komplette Clock-Klasse:
"""
Die Klasse Clock wird verwendet, um eine Uhr zu simulieren.
"""
class Clock(object):
def __init__(self, hours, minutes, seconds):
"""
Die Parameter Stunden, Minuten und Sekunden müssen
ganze Zahlen sein und die folgenden Gleichungen erfüllen:
0 <= h <24
0 <= m <60
0 <= s <60
"""
self.set_Clock(hours, minutes, seconds)
def set_Clock(self, hours, minutes, seconds):
"""
Die Parameter Stunden, Minuten und Sekunden müssen sein
ganze Zahlen und müssen die folgenden Gleichungen erfüllen:
0 <= h <24
0 <= m <60
0 <= s <60
"""
if type(hours) == int and 0 <= hours and hours < 24:
self._hours = hours
else:
raise TypeError("Stunden müssen ganze Zahlen zwischen 0 und 23 sein!")
if type(minutes) == int and 0 <= minutes and minutes < 60:
self.__minutes = minutes
else:
raise TypeError("Minuten müssen ganze Zahlen zwischen 0 und 59 sein!")
if type(seconds) == int and 0 <= seconds and seconds < 60:
self.__seconds = seconds
else:
raise TypeError("Sekunden müssen ganze Zahlen zwischen 0 und 59 sein!")
def __str__(self):
return "{0:02d}:{1:02d}:{2:02d}".format(self._hours,
self.__minutes,
self.__seconds)
def tick(self):
"""
Diese Methode lässt die Uhr "ticken", dies bedeutet, dass die
Die interne Zeit wird um eine Sekunde vorverlegt.
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))
Lassen Sie uns unsere Ausnahmebehandlung überprüfen, indem wir Floats und Strings als Eingabe eingeben. Wir prüfen auch, was passiert, wenn wir die Grenzen der erwarteten Werte überschreiten:
x = Clock(7.7, 45, 17)
x = Clock(24, 45, 17)
x = Clock(23, 60, 17)
x = Clock("23", "60", "17")
x = Clock(23, 17)
Wir werden nun eine Klasse "Calendar" erstellen, die viele Ähnlichkeiten mit der zuvor definierten Clock-Klasse aufweist. Anstelle von "Häkchen" haben wir eine "Voraus" -Methode, bei der das Datum bei jedem Aufruf um einen Tag vorverlegt wird. Das Hinzufügen eines Tages zu einem Datum ist ziemlich schwierig. Wir müssen prüfen, ob das Datum der letzte Tag in einem Monat ist und die Anzahl der Tage in den Monaten variiert. Als ob das nicht schlimm genug wäre, haben wir das Problem mit dem Februar und dem Schaltjahr.
Die Regeln für die Berechnung eines Schaltjahres lauten wie folgt:
- Wenn ein Jahr durch 400 teilbar ist, ist es ein Schaltjahr.
- Wenn ein Jahr nicht durch 400, sondern durch 100 teilbar ist, ist es kein Schaltjahr.
- Eine Jahreszahl, die durch 4, aber nicht durch 100 teilbar ist, ist ein Schaltjahr.
- Alle anderen Jahreszahlen sind übliche Jahre, d. H. Keine Schaltjahre.
Als kleine nützliche Spielerei haben wir die Möglichkeit hinzugefügt, ein Datum entweder im britischen oder im amerikanischen (kanadischen) Stil auszugeben.
"""
Die Klasse Calendar implementiert einen Kalender.
"""
class Calendar(object):
months = (31,28,31,30,31,30,31,31,30,31,30,31)
date_style = "Britisch"
@staticmethod
def leapyear(year):
"""
Die Methode leapyear gibt True zurück, wenn der Parameter year
ist ein Schaltjahr, sonst False
"""
if not year % 4 == 0:
return False
elif not year % 100 == 0:
return True
elif not year % 400 == 0:
return False
else:
return True
def __init__(self, d, m, y):
"""
d, m, y müssen ganzzahlige Werte sein und das Jahr muss sein
eine vierstellige Jahreszahl
"""
self.set_Calendar(d,m,y)
def set_Calendar(self, d, m, y):
"""
d, m, y müssen ganzzahlige Werte sein und das Jahr muss sein
eine vierstellige Jahreszahl
"""
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):
if Calendar.date_style == "Britisch":
return "{0:02d}/{1:02d}/{2:4d}".format(self.__days,
self.__months,
self.__years)
else:
# unter der Annahme eines amerikanischen Stils
return "{0:02d}/{1:02d}/{2:4d}".format(self.__months,
self.__days,
self.__years)
def advance(self):
"""
Diese Methode wird zum nächsten Datum weitergeleitet.
"""
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 kein leapyear: Zahl teilbar durch 100 aber nicht durch 400: ")
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)
print("Wechsel zum amerikanischen Datumsstil: ")
Calendar.date_style = "American"
print("Nach advance: ", x)
Zuletzt werden wir unser Beispiel für Mehrfachvererbung vorstellen. Wir sind jetzt in der Lage, die ursprünglich vorgesehene Klasse CalendarClock zu implementieren, die sowohl von Clock als auch von Calendar erbt. Die Methode "tick" von Clock muss überschrieben werden. Die neue Tick-Methode von CalendarClock muss jedoch die Tick-Methode von Clock aufrufen: Clock.tick (self)
"""
Modul, das die Klasse CalendarClock implementiert.
"""
from clock import Clock
from calender import Calendar
class CalendarClock(Clock, Calendar):
"""
Die Klasse CalendarClock implementiert eine Uhr mit integrierter
Kalender. Es handelt sich um eine Mehrfachvererbung, da diese erbt
sowohl von der Clock als auch vom Calendar
"""
def __init__(self, day, month, year, hour, minute, second):
Clock.__init__(self,hour, minute, second)
Calendar.__init__(self, day, month, year)
def tick(self):
"""
Stellen Sie die Uhr um eine Sekunde vor
"""
previous_hour = self._hours
Clock.tick(self)
if (self._hours < previous_hour):
self.advance()
def __str__(self):
return Calendar.__str__(self) + ", " + Clock.__str__(self)
if __name__ == "__main__":
x = CalendarClock(31, 12, 2013, 23, 59, 59)
print("Ein Tick von ",x, end=" ")
x.tick()
print("nach ", x)
x = CalendarClock(28, 2, 1900, 23, 59, 59)
print("Ein Tick von ",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("Ein Tick von ",x, end=" ")
x.tick()
print("nach ", x)
Das Diamantproblem oder der tödliche Diamant des Todes ''
Das "Diamantproblem" (manchmal als "tödlicher Diamant des Todes" bezeichnet) ist der allgemein verwendete Begriff für eine Mehrdeutigkeit, die entsteht, wenn zwei Klassen B und C von einer Oberklasse A und eine andere Klasse D sowohl von B als auch von C erben. Wenn es in A eine Methode "m" gibt, die B oder C (oder sogar beide) überschrieben hat, und wenn diese Methode nicht überschrieben wird, stellt sich die Frage, welche Version der Methode D erbt. Es könnte der von A, B oder C sein.
Schauen wir uns Python an. Die erste Diamond Problem-Konfiguration sieht folgendermaßen aus: Sowohl B als auch C überschreiben die Methode m von A:
class A:
def m(self):
print("m von A ruft")
class B(A):
def m(self):
print("m von B ruft")
class C(A):
def m(self):
print("m von C ruft")
class D(B,C):
pass
Wenn Sie die Methode m für eine Instanz x von D aufrufen, d. H. X.m (), erhalten wir die Ausgabe "m von B aufgerufen". Wenn wir die Reihenfolge der Klassen im Klassenkopf von D in "Klasse D (C, B):" transponieren, erhalten wir die Ausgabe "m von C aufgerufen".
Der Fall, in dem m nur in einer der Klassen B oder C überschrieben wird, z. in C:
class A:
def m(self):
print("m von A ruft")
class B(A):
pass
class C(A):
def m(self):
print("m von C ruft")
class D(B,C):
pass
x = D()
x.m()
Grundsätzlich sind zwei Möglichkeiten vorstellbar: "m von C" oder "m von A" könnten verwendet werden
Wir rufen dieses Skript mit Python2.7 (Python) und Python3 (Python3) auf, um zu sehen, was passiert:
$ python diamant1.py m von A ruft $ python3 diamant1.py m von C ruft
Nur für diejenigen, die sich für Python Version 2 interessieren: Um in Python2 dasselbe Vererbungsverhalten wie in Python3 zu haben, muss jede Klasse von der Klasse "Objekt" erben. Unsere Klasse A erbt nicht vom Objekt, daher erhalten wir eine sogenannte Old-Style-Klasse, wenn wir das Skript mit python2 aufrufen. Die Mehrfachvererbung mit Klassen alten Stils unterliegt zwei Regeln: Tiefe zuerst und dann von links nach rechts. Wenn Sie die Kopfzeile von A in "Klasse A (Objekt):" ändern, haben wir in beiden Python-Versionen das gleiche Verhalten.
super und MRO
Wir haben in unserer vorherigen Implementierung des Diamantproblems gesehen, wie Python das Problem "löst", d. H. In welcher Reihenfolge die Basisklassen durchsucht werden. Die Reihenfolge wird durch die sogenannte "Method Resolution Order" oder kurz MRO * definiert.
Wir werden unser vorheriges Beispiel erweitern, sodass jede Klasse ihre eigene Methode m definiert:
class A:
def m(self):
print("m von A ruft")
class B(A):
def m(self):
print("m von B ruft")
class C(A):
def m(self):
print("m von C ruft")
class D(B,C):
def m(self):
print("m von D ruft")
Wenden wir die Methode m auf eine Instanz von D an. Wir können sehen, dass nur der Code der Methode m von D ausgeführt wird. Wir können die Methoden m der anderen Klassen auch explizit über den Klassennamen aufrufen, wie wir in der folgenden interaktiven Python-Sitzung demonstrieren:
from super1 import A,B,C,D
x = D()
B.m(x)
C.m(x)
A.m(x)
Nehmen wir nun an, dass die Methode m von D beim Aufruf auch den Code von m von B, C und A ausführen soll. Wir könnten es so umsetzen:
class D(B,C):
def m(self):
print("m of D ruft")
B.m(self)
C.m(self)
A.m(self)
Die Ausgabe ist wie erwartet:
from mro import D
x = D()
x.m()
Aber es stellt sich erneut heraus, dass die Dinge komplizierter sind, als sie scheinen. Wie können wir mit der Situation umgehen, wenn sowohl m von B als auch m von C auch m von A nennen müssen? In diesem Fall müssen wir den Aufruf A.m (self) von m in D entfernen. Der Code könnte so aussehen, aber es lauert immer noch ein Fehler darin:
class A:
def m(self):
print("m of A ruft")
class B(A):
def m(self):
print("m of B ruft")
A.m(self)
class C(A):
def m(self):
print("m of C ruft")
A.m(self)
class D(B,C):
def m(self):
print("m of D ruft")
B.m(self)
C.m(self)
Der Fehler ist, dass die Methode m von A zweimal aufgerufen wird:
from super3 import D
x = D()
x.m()
Eine Möglichkeit, dieses Problem zu lösen - zugegebenermaßen keine pythonische - besteht darin, die Methoden m von B und C in zwei Methoden aufzuteilen. Die erste Methode mit dem Namen _m
besteht aus dem spezifischen Code für B und C und die andere Methode heißt immer noch m, besteht aber jetzt aus einem Aufruf `self._m ()`
und einem Aufruf A.m(self)
. Der Code der Methode m von D besteht nun aus dem spezifischen Code von D 'print ("m von D ruft")' und den Aufrufen B._m(self)
, C._m(self)
und A.m(self)
:
class A:
def m(self):
print("m von A ruft")
class B(A):
def _m(self):
print("m von B ruft")
def m(self):
self._m()
A.m(self)
class C(A):
def _m(self):
print("m von C ruft")
def m(self):
self._m()
A.m(self)
class D(B,C):
def m(self):
print("m von D ruft")
B._m(self)
C._m(self)
A.m(self)
Unser Problem ist gelöst, aber - wie bereits erwähnt - nicht pythonisch:
from super4 import D
x = D()
x.m()
Der optimale Weg, um das Problem zu lösen, der "super" pythonische Weg, wäre der Aufruf der Superfunktion:
class A:
def m(self):
print("m von A ruft")
class B(A):
def m(self):
print("m von B ruft")
super().m()
class C(A):
def m(self):
print("m von C ruft")
super().m()
class D(B,C):
def m(self):
print("m von D ruft")
super().m()
Es löst auch unser Problem, aber auch in einem schönen Design:
x = D()
x.m()
Die Superfunktion wird häufig verwendet, wenn Instanzen mit der Methode __init__
initialisiert werden:
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__()
Wir zeigen die Arbeitsweise in der folgenden interaktiven Sitzung:
d = D()
c = C()
b = B()
a = A()
Es stellt sich die Frage, wie die Superfunktionen Entscheidungen treffen. Wie entscheidet es, welche Klasse verwendet werden soll? Wie bereits erwähnt, wird die sogenannte Method Resolution Order (MRO) verwendet. Es basiert auf dem Algorithmus "C3 Superclass Linearization". Dies wird als Linearisierung bezeichnet, da die Baumstruktur in eine lineare Reihenfolge unterteilt ist. Die mro-Methode kann verwendet werden, um diese Liste zu erstellen:
D.mro()
B.mro()
A.mro()
Polymorphismus
Polymorphismus wird aus zwei griechischen Wörtern konstruiert. "Poly" steht für "viel" oder "viele" und "Morph" bedeutet Form oder Gestalt. Polymorphismus ist der Zustand oder die Bedingung, polymorph zu sein, oder wenn wir die Übersetzungen der Komponenten verwenden, "die Fähigkeit, in vielen Formen oder Formen zu sein. Polymorphismus ist ein Begriff, der in vielen wissenschaftlichen Bereichen verwendet wird. In der Kristallographie definiert er den Zustand, wenn etwas kristallisiert in zwei oder mehr chemisch identische, aber kristallographisch unterschiedliche Formen. Biologen kennen Polymorphismus als die Existenz eines Organismus in verschiedenen Formen oder Farbvarianten. Die Römer hatten sogar einen Gott namens Morpheus, der jede menschliche Form annehmen kann: Morheus erscheint in Ovids verwandelt sich und ist der Sohn von Somnus, dem Gott des Schlafes. Sie können Morpheus und Iris auf dem Bild auf der rechten Seite bewundern.
Bevor wir einschlafen, kehren wir zu Python zurück und zu dem, was Polymorphismus im Kontext von Programmiersprachen bedeutet. Polymorphismus in der Informatik ist die Fähigkeit, dieselbe Schnittstelle für unterschiedliche zugrunde liegende Formen darzustellen. Wir können zum Beispiel in einigen Programmiersprachen polymorphe Funktionen oder Methoden haben. Polymorphe Funktionen oder Methoden können auf Argumente unterschiedlichen Typs angewendet werden und sie können sich je nach Art der Argumente, auf die sie angewendet werden, unterschiedlich verhalten. Wir können den gleichen Funktionsnamen auch mit einer variierenden Anzahl von Parametern definieren.
Schauen wir uns die folgende Python-Funktion an:
def f(x, y):
print("Werte: ", x, y)
f(42, 43)
f(42, 43.7)
f(42.3, 43)
f(42.0, 43.9)
Wir können diese Funktion mit verschiedenen Typen aufrufen, wie im Beispiel gezeigt. In typisierten Programmiersprachen wie Java oder C ++ müssten wir f überladen, um die verschiedenen Typkombinationen zu implementieren.
Unser Beispiel könnte folgendermaßen in C ++ implementiert werden:
#includeusing 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); }
Python ist implizit polymorph. Wir können unsere zuvor definierte Funktion f sogar auf Listen, Zeichenfolgen oder andere Typen anwenden, die gedruckt werden können:
def f(x,y):
print("Werte: ", x, y)
f([3,5,6],(3,5))
f("Eine Zeichenkette", ("Ein Tupel", "mit Zeichenketten"))
f({2,3,9}, {"a":3.4,"b":7.8, "c":9.04})