Angenommen, Sie haben die folgende Situation
#include <iostream>
class Animal {
public:
virtual void speak() = 0;
};
class Dog : public Animal {
void speak() { std::cout << "woff!" <<std::endl; }
};
class Cat : public Animal {
void speak() { std::cout << "meow!" <<std::endl; }
};
void makeSpeak(Animal &a) {
a.speak();
}
int main() {
Dog d;
Cat c;
makeSpeak(d);
makeSpeak(c);
}
Wie Sie sehen können, ist makeSpeak eine Routine, die ein generisches Animal-Objekt akzeptiert. In diesem Fall ist Animal einer Java-Schnittstelle ziemlich ähnlich, da es nur eine rein virtuelle Methode enthält. makeSpeak kennt die Art des Tieres, an dem es übergeben wird, nicht. Es sendet nur das Signal "speak" und verlässt die späte Bindung, um sich darum zu kümmern, welche Methode aufgerufen werden soll: entweder Cat :: speak () oder Dog :: speak (). Dies bedeutet, dass für makeSpeak die Kenntnis, welche Unterklasse tatsächlich übergeben wird, irrelevant ist.
Aber was ist mit Python? Sehen wir uns den Code für denselben Fall in Python an. Bitte beachten Sie, dass ich für einen Moment versuche, dem C ++ - Fall so ähnlich wie möglich zu sein:
class Animal(object):
def speak(self):
raise NotImplementedError()
class Dog(Animal):
def speak(self):
print "woff!"
class Cat(Animal):
def speak(self):
print "meow"
def makeSpeak(a):
a.speak()
d=Dog()
c=Cat()
makeSpeak(d)
makeSpeak(c)
In diesem Beispiel sehen Sie dieselbe Strategie. Sie verwenden die Vererbung, um das hierarchische Konzept von Hunden und Katzen als Tiere zu nutzen. In Python ist diese Hierarchie jedoch nicht erforderlich. Das funktioniert genauso gut
class Dog:
def speak(self):
print "woff!"
class Cat:
def speak(self):
print "meow"
def makeSpeak(a):
a.speak()
d=Dog()
c=Cat()
makeSpeak(d)
makeSpeak(c)
In Python können Sie das Signal "Sprechen" an jedes gewünschte Objekt senden. Wenn das Objekt damit umgehen kann, wird es ausgeführt, andernfalls wird eine Ausnahme ausgelöst. Angenommen, Sie fügen beiden Codes eine Klasse Flugzeug hinzu und senden ein Flugzeugobjekt an makeSpeak. Im C ++ - Fall wird es nicht kompiliert, da Airplane keine abgeleitete Tierklasse ist. Im Fall von Python wird zur Laufzeit eine Ausnahme ausgelöst, die sogar ein erwartetes Verhalten sein kann.
Angenommen, Sie fügen auf der anderen Seite eine MouthOfTruth-Klasse mit einer Methode speak () hinzu. Im C ++ - Fall müssen Sie entweder Ihre Hierarchie umgestalten oder eine andere makeSpeak-Methode definieren, um MouthOfTruth-Objekte zu akzeptieren, oder Sie können in Java das Verhalten in ein CanSpeakIface extrahieren und die Schnittstelle für jedes implementieren. Es gibt viele Lösungen ...
Ich möchte darauf hinweisen, dass ich noch keinen einzigen Grund gefunden habe, die Vererbung in Python zu verwenden (abgesehen von Frameworks und Ausnahmebäumen, aber ich denke, dass es alternative Strategien gibt). Sie müssen keine von der Basis abgeleitete Hierarchie implementieren, um eine polymorphe Leistung zu erzielen. Wenn Sie die Vererbung verwenden möchten, um die Implementierung wiederzuverwenden, können Sie dies durch Eindämmung und Delegierung erreichen. Der zusätzliche Vorteil besteht darin, dass Sie sie zur Laufzeit ändern und die Schnittstelle des enthaltenen Systems klar definieren können, ohne unbeabsichtigte Nebenwirkungen zu riskieren.
Am Ende stellt sich also die Frage: Was ist der Sinn der Vererbung in Python?
Edit : danke für die sehr interessanten Antworten. Sie können es zwar für die Wiederverwendung von Code verwenden, aber ich bin immer vorsichtig, wenn Sie die Implementierung wiederverwenden. Im Allgemeinen neige ich dazu, sehr flache Vererbungsbäume oder gar keinen Baum zu erstellen, und wenn eine Funktionalität gemeinsam ist, überarbeite ich sie als gemeinsame Modulroutine und rufe sie dann von jedem Objekt aus auf. Ich sehe den Vorteil, einen einzigen Änderungspunkt zu haben (z. B. anstatt Hund, Katze, Elch usw. hinzuzufügen, füge ich nur Tier hinzu, was der grundlegende Vorteil der Vererbung ist), aber Sie können dasselbe mit erreichen eine Delegierungskette (z. B. a la JavaScript). Ich behaupte nicht, dass es besser ist, nur auf eine andere Weise.
Ich habe diesbezüglich auch einen ähnlichen Beitrag gefunden .
quelle
Antworten:
Sie bezeichnen die Enten-Typisierung zur Laufzeit als "überschreibende" Vererbung. Ich glaube jedoch, dass Vererbung als Entwurfs- und Implementierungsansatz ihre eigenen Vorzüge hat und ein wesentlicher Bestandteil des objektorientierten Entwurfs ist. Meiner bescheidenen Meinung nach ist die Frage, ob Sie etwas anderes erreichen können, nicht sehr relevant, da Sie Python tatsächlich ohne Klassen, Funktionen und mehr codieren könnten, aber die Frage ist, wie gut gestaltet, robust und lesbar Ihr Code sein wird.
Ich kann zwei Beispiele dafür nennen, wo Vererbung meiner Meinung nach der richtige Ansatz ist. Ich bin sicher, dass es noch mehr gibt.
Wenn Sie mit Bedacht codieren, möchte Ihre makeSpeak-Funktion möglicherweise überprüfen, ob es sich bei der Eingabe tatsächlich um ein Tier handelt und nicht nur darum, dass "es sprechen kann". In diesem Fall wäre die eleganteste Methode die Verwendung der Vererbung. Auch hier können Sie dies auf andere Weise tun, aber das ist das Schöne an objektorientiertem Design mit Vererbung - Ihr Code prüft "wirklich", ob die Eingabe ein "Tier" ist.
Zweitens und deutlich einfacher ist die Kapselung - ein weiterer wesentlicher Bestandteil des objektorientierten Designs. Dies wird relevant, wenn der Vorfahr Datenelemente und / oder nicht abstrakte Methoden hat. Nehmen Sie das folgende dumme Beispiel, in dem der Vorfahr eine Funktion (speak_twice) hat, die eine dann abstrakte Funktion aufruft:
Vorausgesetzt, es
"speak_twice"
handelt sich um eine wichtige Funktion, möchten Sie sie nicht sowohl in Dog als auch in Cat codieren, und ich bin sicher, dass Sie dieses Beispiel extrapolieren können. Sicher, Sie könnten eine eigenständige Python-Funktion implementieren, die ein Objekt vom Typ Ente akzeptiert, prüfen, ob es eine Sprechfunktion hat, und es zweimal aufrufen, aber das ist nicht elegant und verfehlt Punkt 1 (überprüfen Sie, ob es sich um ein Tier handelt). Schlimmer noch, und um das Beispiel der Kapselung zu stärken, was wäre, wenn eine Mitgliedsfunktion in der Nachkommenklasse verwenden wollte"speak_twice"
?Es wird noch deutlicher, wenn die Vorfahrenklasse ein Datenelement hat, das beispielsweise
"number_of_legs"
von nicht abstrakten Methoden im Vorfahren wie verwendet wird"print_number_of_legs"
, aber im Konstruktor der Nachkommenklasse initiiert wird (z. B. würde Dog es mit 4 initialisieren, während Snake es initialisieren würde mit 0).Ich bin mir wieder sicher, dass es unendlich viele weitere Beispiele gibt, aber im Grunde erfordert jede (groß genug) Software, die auf einem soliden objektorientierten Design basiert, eine Vererbung.
quelle
Bei der Vererbung in Python dreht sich alles um die Wiederverwendung von Code. Faktorisieren Sie allgemeine Funktionen in eine Basisklasse und implementieren Sie unterschiedliche Funktionen in den abgeleiteten Klassen.
quelle
Die Vererbung in Python ist mehr eine Bequemlichkeit als alles andere. Ich finde, dass es am besten verwendet wird, um eine Klasse mit "Standardverhalten" zu versehen.
In der Tat gibt es eine bedeutende Community von Python-Entwicklern, die sich überhaupt gegen die Verwendung von Vererbung aussprechen. Was auch immer Sie tun, übertreiben Sie es nicht. Eine übermäßig komplizierte Klassenhierarchie ist ein sicherer Weg, um als "Java-Programmierer" bezeichnet zu werden, und das kann man einfach nicht haben. :-)
quelle
Ich denke, der Punkt der Vererbung in Python besteht nicht darin, den Code kompilieren zu lassen, sondern aus dem wahren Grund der Vererbung die Klasse in eine andere untergeordnete Klasse zu erweitern und die Logik in der Basisklasse zu überschreiben. Die Enten-Eingabe in Python macht das "Interface" -Konzept jedoch unbrauchbar, da Sie vor dem Aufruf einfach überprüfen können, ob die Methode vorhanden ist, ohne eine Schnittstelle zum Einschränken der Klassenstruktur verwenden zu müssen.
quelle
Ich denke, dass es sehr schwierig ist, mit solchen abstrakten Beispielen eine aussagekräftige, konkrete Antwort zu geben ...
Zur Vereinfachung gibt es zwei Arten der Vererbung: Schnittstelle und Implementierung. Wenn Sie die Implementierung erben müssen, unterscheidet sich Python nicht so stark von statisch typisierten OO-Sprachen wie C ++.
Bei der Vererbung der Benutzeroberfläche gibt es einen großen Unterschied, der meiner Erfahrung nach grundlegende Konsequenzen für das Design Ihrer Software hat. Sprachen wie Python zwingen Sie in diesem Fall nicht zur Verwendung der Vererbung, und das Vermeiden der Vererbung ist in den meisten Fällen ein guter Punkt, da es sehr schwierig ist, dort später eine falsche Entwurfsauswahl zu korrigieren. Das ist ein bekannter Punkt, der in jedem guten OOP-Buch angesprochen wird.
Es gibt Fälle, in denen die Verwendung der Vererbung für Schnittstellen in Python ratsam ist, z. B. für Plug-Ins usw. In diesen Fällen fehlt Python 2.5 und darunter ein "integrierter" eleganter Ansatz, und mehrere große Frameworks haben ihre eigenen Lösungen entwickelt (Zope, Trac, Twister). Python 2.6 und höher verfügt über ABC-Klassen, um dies zu lösen .
quelle
Es ist keine Vererbung, die das Eingeben von Enten sinnlos macht, sondern Schnittstellen - wie die, die Sie bei der Erstellung einer vollständig abstrakten Tierklasse ausgewählt haben.
Wenn Sie eine Tierklasse verwendet hätten, die ein reales Verhalten für ihre Nachkommen einführt, dann hätten Hunde- und Katzenklassen, die ein zusätzliches Verhalten eingeführt haben, einen Grund für beide Klassen. Nur wenn die Vorfahrenklasse keinen tatsächlichen Code zu den Nachkommenklassen beiträgt, ist Ihr Argument korrekt.
Da Python die Funktionen eines Objekts direkt kennen kann und diese Funktionen über die Klassendefinition hinaus veränderbar sind, ist die Idee, dem Programm mithilfe einer rein abstrakten Schnittstelle zu "sagen", welche Methoden aufgerufen werden können, etwas sinnlos. Aber das ist nicht der einzige oder sogar der Hauptpunkt der Vererbung.
quelle
In C ++ / Java / etc wird Polymorphismus durch Vererbung verursacht. Geben Sie diesen missverstandenen Glauben auf, und dynamische Sprachen öffnen sich Ihnen.
Im Wesentlichen gibt es in Python keine Schnittstelle, sondern "das Verständnis, dass bestimmte Methoden aufrufbar sind". Ziemlich handgewellt und akademisch klingend, nein? Dies bedeutet, dass Sie, da Sie "Sprechen" aufrufen, eindeutig erwarten, dass das Objekt eine "Sprechen" -Methode haben sollte. Einfach, oder? Dies ist insofern sehr Liskov-ian, als die Benutzer einer Klasse ihre Schnittstelle definieren, ein gutes Designkonzept, das Sie zu einer gesünderen TDD führt.
Was also übrig bleibt, ist, wie ein anderes Poster höflich vermeiden konnte, ein Code-Sharing-Trick. Sie könnten das gleiche Verhalten in jede "untergeordnete" Klasse schreiben, aber das wäre überflüssig. Einfachere Vererbung oder Einmischung von Funktionen, die in der gesamten Vererbungshierarchie unveränderlich sind. Kleinerer DRY-er-Code ist im Allgemeinen besser.
quelle
Sie können die Vererbung in Python und so ziemlich jeder anderen Sprache umgehen. Es geht jedoch nur um die Wiederverwendung von Code und die Vereinfachung des Codes.
Nur ein semantischer Trick, aber nachdem Sie Ihre Klassen und Basisklassen erstellt haben, müssen Sie nicht einmal wissen, was mit Ihrem Objekt möglich ist, um zu sehen, ob Sie dies tun können.
Angenommen, Sie haben d, das ist ein Hund, der Tier untergeordnet hat.
Wenn der vom Benutzer eingegebene Wert verfügbar ist, führt der Code die richtige Methode aus.
Auf diese Weise können Sie eine beliebige Kombination aus Säugetier-, Reptilien- und Vogel-Hybridmonstrosität erstellen, die Sie möchten, und jetzt können Sie festlegen, dass "Rinde!" während des Fliegens und Ausstrecken der gespaltenen Zunge und es wird richtig damit umgehen! Viel Spass damit!
quelle
Ich sehe nicht viel Sinn in der Vererbung.
Jedes Mal, wenn ich Vererbung in realen Systemen verwendet habe, wurde ich verbrannt, weil dies zu einem Wirrwarr von Abhängigkeiten führte, oder ich erkannte einfach mit der Zeit, dass ich ohne sie viel besser dran wäre. Jetzt vermeide ich es so weit wie möglich. Ich habe einfach nie eine Verwendung dafür.
James Gosling wurde einmal auf einer Pressekonferenz eine Frage in der Art gestellt: "Wenn Sie zurückgehen und Java anders machen könnten, was würden Sie weglassen?". Seine Antwort war "Klassen", über die gelacht wurde. Er meinte es jedoch ernst und erklärte, dass es wirklich nicht um Klassen ging, sondern um die Vererbung.
Ich betrachte es als eine Art Drogenabhängigkeit - es gibt Ihnen eine schnelle Lösung, die sich gut anfühlt, aber am Ende bringt es Sie durcheinander. Damit meine ich, dass es eine bequeme Möglichkeit ist, Code wiederzuverwenden, aber es erzwingt eine ungesunde Kopplung zwischen Kind- und Elternklasse. Änderungen am Elternteil können das Kind beschädigen. Das Kind ist für bestimmte Funktionen vom übergeordneten Element abhängig und kann diese Funktionalität nicht ändern. Daher ist die vom Kind bereitgestellte Funktionalität auch an das Elternteil gebunden - Sie können nur beides haben.
Besser ist es, eine einzelne Client-Klasse für eine Schnittstelle bereitzustellen, die die Schnittstelle implementiert, wobei die Funktionalität anderer Objekte verwendet wird, die zur Konstruktionszeit erstellt werden. Wenn Sie dies über ordnungsgemäß gestaltete Schnittstellen tun, kann jede Kopplung beseitigt werden, und wir bieten eine hochkompositive API (dies ist nichts Neues - die meisten Programmierer tun dies bereits, nur nicht genug). Beachten Sie, dass die implementierende Klasse die Funktionalität nicht einfach verfügbar machen darf, andernfalls sollte der Client die zusammengesetzten Klassen nur direkt verwenden - er muss etwas Neues tun, indem er diese Funktionalität kombiniert.
Es gibt das Argument des Vererbungslagers, dass reine Delegierungsimplementierungen darunter leiden, weil sie viele "Klebemethoden" erfordern, die einfach Werte durch eine Delegierungskette weitergeben. Dies bedeutet jedoch lediglich, ein vererbungsähnliches Design mithilfe der Delegierung neu zu erfinden. Programmierer mit zu vielen Jahren Erfahrung mit vererbungsbasierten Designs sind besonders anfällig dafür, in diese Falle zu tappen, da sie, ohne es zu merken, darüber nachdenken, wie sie etwas mithilfe von Vererbung implementieren und es dann in Delegierung umwandeln würden.
Für eine ordnungsgemäße Trennung von Bedenken wie dem obigen Code sind keine Klebemethoden erforderlich, da jeder Schritt tatsächlich einen Mehrwert darstellt. Es handelt sich also überhaupt nicht um Klebemethoden (wenn sie keinen Mehrwert bieten, ist das Design fehlerhaft).
Es läuft darauf hinaus:
Für wiederverwendbaren Code sollte jede Klasse nur eines tun (und es gut machen).
Durch Vererbung werden Klassen erstellt, die mehr als eine Aufgabe erfüllen, da sie mit übergeordneten Klassen verwechselt werden.
Daher führt die Verwendung der Vererbung dazu, dass Klassen schwer wiederzuverwenden sind.
quelle
Ein weiterer kleiner Punkt ist, dass Sie im 3. Beispiel von op isinstance () nicht aufrufen können. Wenn Sie beispielsweise Ihr drittes Beispiel an ein anderes Objekt übergeben, das Anrufe entgegennimmt und "Tier" eingibt, sprechen Sie darüber. Wenn Sie dies nicht tun, müssen Sie nach Hundetyp, Katzentyp usw. suchen. Ich bin mir nicht sicher, ob die Instanzprüfung aufgrund der späten Bindung wirklich "Pythonic" ist. Aber dann müsste man so implementieren, dass das AnimalControl nicht versucht, Cheeseburger-Typen in den Truck zu werfen, weil Cheeseburger nicht sprechen.
quelle
Klassen in Python sind im Grunde nur Möglichkeiten, eine Reihe von Funktionen und Daten zu gruppieren. Sie unterscheiden sich von Klassen in C ++ und dergleichen.
Ich habe meistens Vererbung gesehen, die zum Überschreiben von Methoden der Superklasse verwendet wurde. Zum Beispiel wäre vielleicht eine pythonischere Verwendung der Vererbung ..
Natürlich sind Katzen keine Art von Hund, aber ich habe diese
Dog
Klasse (von Drittanbietern) , die perfekt funktioniert, mit Ausnahme derspeak
Methode, die ich überschreiben möchte - dies erspart die erneute Implementierung der gesamten Klasse, nur damit sie miaut. Auch hierCat
ist zwar keine Art vonDog
, aber eine Katze erbt viele Attribute.Ein viel besseres (praktisches) Beispiel für das Überschreiben einer Methode oder eines Attributs ist das Ändern des Benutzeragenten für urllib. Sie unterteilen
urllib.FancyURLopener
und ändern grundsätzlich das Versionsattribut ( aus der Dokumentation ):Eine andere Art und Weise, wie Ausnahmen verwendet werden, ist für Ausnahmen, wenn die Vererbung "richtiger" verwendet wird:
..Sie können dann fangen
AnimalError
, um alle Ausnahmen zu fangen, die davon erben, oder eine bestimmte wieAnimalBrokenLegError
quelle