Frage Was ist der Vererbungsort in Python?


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 sehr ähnlich, da es nur eine rein virtuelle Methode enthält. makeSpeak kennt die Art des Tieres nicht, das es passiert. Es sendet nur das Signal "sprechen" und hinterlässt die späte Bindung, um sich darum zu kümmern, welche Methode aufgerufen werden soll: entweder Cat :: speak () oder Dog :: speak (). Das heißt, was makeSpeak betrifft, ist das Wissen, welche Subklasse tatsächlich übergeben wird, irrelevant.

Aber was ist mit Python? Lassen Sie uns den Code für den gleichen Fall in Python sehen. Bitte beachten Sie, dass ich versuche, für einen Moment 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 die gleiche Strategie. Sie nutzen die Vererbung, um das hierarchische Konzept von Hunden und Katzen, die Tiere sind, zu nutzen. In Python ist diese Hierarchie 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 beliebige 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 Fall C ++ wird es nicht kompiliert, da Airplane keine abgeleitete Klasse von Animal ist. Im Python-Fall wird zur Laufzeit eine Ausnahme ausgelöst, was sogar ein erwartetes Verhalten sein könnte.

Angenommen, Sie fügen eine MouthOfTruth-Klasse mit einer Methode speak () hinzu. Im Fall C ++ müssen Sie entweder Ihre Hierarchie umgestalten, oder Sie müssen eine andere makeSpeak-Methode definieren, um MouthOfTruth-Objekte zu akzeptieren, oder in Java können Sie das Verhalten in eine CanSpeakIface extrahieren und die Schnittstelle für jede implementieren. Es gibt viele Lösungen ...

Worauf ich hinweisen möchte ist, dass ich bisher keinen einzigen Grund gefunden habe, Vererbung in Python zu verwenden (abgesehen von Frameworks und Bäumen von Ausnahmen, aber ich denke, dass alternative Strategien existieren). Sie müssen keine Basis-abgeleitete Hierarchie implementieren, um polymorph zu arbeiten. Wenn Sie die Implementation mithilfe von Vererbung wiederverwenden möchten, können Sie dies auch durch Containment und Delegierung erreichen, mit dem zusätzlichen Vorteil, dass Sie sie zur Laufzeit ändern können und die Schnittstelle des Containers eindeutig definieren, ohne unbeabsichtigte Nebenwirkungen zu riskieren.

Am Ende steht die Frage: Was ist der Vererbungsgrund in Python?

Bearbeiten: Danke für die sehr interessanten Antworten. In der Tat können Sie es für die Wiederverwendung von Code verwenden, aber ich bin immer vorsichtig, wenn ich die Implementierung wiederverwende. Im Allgemeinen tendiere ich dazu, sehr flache Vererbungsbäume oder überhaupt keinen Baum zu machen, und wenn eine Funktionalität üblich ist, refactoriere ich sie als eine übliche Modulroutine aus und rufe sie dann von jedem Objekt ab. Ich sehe den Vorteil, einen einzelnen Punkt der Änderung zu haben (z. B. anstatt zu Hund, Katze, Elch und so weiter hinzuzufügen, ich füge einfach Tier hinzu, was der Hauptvorteil der Vererbung ist), aber Sie können dasselbe mit erreichen eine Delegationskette (zB a la JavaScript). Ich behaupte nicht, dass es besser ist, nur auf eine andere Art und Weise.

Ich habe auch gefunden ein ähnlicher Beitrag In dieser hinsicht.


76
2018-06-19 23:28


Ursprung


Antworten:


Sie beziehen sich auf die Runtime-Duck-Typisierung als "übergeordnete" Vererbung, aber ich glaube, dass die Vererbung ihre eigenen Vorzüge als Design- und Implementierungsansatz hat und ein wesentlicher Bestandteil des objektorientierten Designs ist. Meiner bescheidenen Meinung nach ist die Frage, ob man etwas anderes erreichen kann, nicht sehr relevant, denn eigentlich könnte man Python ohne Klassen, Funktionen und mehr programmieren, aber die Frage ist, wie gut gestaltet, robust und lesbar Ihr Code sein wird.

Ich kann zwei Beispiele dafür geben, wo Vererbung meiner Meinung nach der richtige Ansatz ist. Ich bin mir sicher, dass es mehr gibt.

Erstens, wenn Sie mit Bedacht programmieren, möchte Ihre makeSpeak-Funktion vielleicht überprüfen, dass ihre Eingabe tatsächlich ein Animal ist, und nicht nur, dass "es sprechen kann". In diesem Fall wäre die eleganteste Methode die Verwendung der Vererbung. Wiederum können Sie es auf andere Weise tun, aber das ist das Schöne an objektorientiertem Design mit Vererbung - Ihr Code wird "wirklich" prüfen, ob die Eingabe ein "Tier" ist.

Zweitens und deutlich übersichtlicher ist Encapsulation - ein weiterer integraler Bestandteil des objektorientierten Designs. Dies wird relevant, wenn der Vorgänger Datenelemente und / oder nicht abstrakte Methoden besitzt. Nehmen Sie das folgende alberne Beispiel, in dem der Vorgänger eine Funktion (speak_twice) hat, die eine then-abstract-Funktion aufruft:

class Animal(object):
    def speak(self):
        raise NotImplementedError()

    def speak_twice(self):
        self.speak()
        self.speak()

class Dog(Animal):
    def speak(self):
        print "woff!"

class Cat(Animal):
    def speak(self):
        print "meow"

Angenommen "speak_twice" ist ein wichtiges Feature, Sie möchten es nicht in Hund und Katze codieren, und ich bin sicher, dass Sie dieses Beispiel extrapolieren können. Sicher, Sie könnten eine Standalone-Funktion von Python implementieren, die ein entenartiges Objekt akzeptiert, überprüft, ob es eine Sprechfunktion hat und es zweimal aufruft, aber das ist beides nicht elegant und vermisst Punkt Nummer 1 (validiere, dass es ein Tier ist). Noch schlimmer, und um das Encapsulation-Beispiel zu verstärken, was wäre, wenn eine Member-Funktion in der Nachkommenklasse verwenden wollte "speak_twice"?

Es wird noch deutlicher, wenn die Vorfahrklasse beispielsweise ein Datenelement besitzt "number_of_legs" das wird von nicht abstrakten Methoden im Vorfahren verwendet "print_number_of_legs", wird aber im Konstruktor der Nachkommenklasse initiiert (z. B. Dog würde es mit 4 initialisieren, während Snake es mit 0 initialisieren würde).

Ich bin mir sicher, dass es unendlich viele weitere Beispiele gibt, aber grundsätzlich wird jede (groß genug) Software, die auf solidem objektorientiertem Design basiert, vererbt.


79
2018-06-19 23:35



Bei der Vererbung in Python geht es um die Wiederverwendung von Code. Faktorisieren Sie allgemeine Funktionen in eine Basisklasse und implementieren Sie unterschiedliche Funktionen in den abgeleiteten Klassen.


12
2018-06-20 04:51



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 Gemeinschaft von Python-Entwicklern, die gegen die Verwendung von Vererbung argumentieren. Was immer du tust, übertreibe es nicht einfach. Eine zu komplizierte Klassenhierarchie zu haben, ist ein sicherer Weg, um einen "Java-Programmierer" zu bekommen, und das kann man einfach nicht haben. :-)


9
2018-06-20 03:44



Ich denke, der Punkt der Vererbung in Python besteht nicht darin, den Code kompilieren zu lassen, sondern es ist der eigentliche Grund der Vererbung, der die Klasse in eine andere Kindklasse erweitert und die Logik in der Basisklasse außer Kraft setzt. Die in Python eingegebene Ente macht das Konzept "interface" jedoch nutzlos, weil Sie einfach prüfen können, ob die Methode vor dem Aufruf existiert, ohne dass Sie eine Schnittstelle verwenden müssen, um die Klassenstruktur einzuschränken.


8
2018-06-19 23:34



Ich halte es für sehr schwierig, mit solchen abstrakten Beispielen eine sinnvolle, 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 sehr von statisch typisierten OO-Sprachen wie C ++.

Die Vererbung der Schnittstelle ist dort, wo es einen großen Unterschied gibt, mit grundlegenden Konsequenzen für das Design Ihrer Software in meiner Erfahrung. Sprachen wie Python zwingen Sie in diesem Fall nicht zur Vererbung, und die Vermeidung von Vererbung ist in den meisten Fällen ein guter Punkt, da es später sehr schwer ist, eine falsche Designauswahl zu korrigieren. Das ist ein bekannter Punkt in einem guten OOP-Buch.

Es gibt Fälle, in denen die Verwendung von Vererbung für Schnittstellen in Python ratsam ist, zum Beispiel für Plug-Ins usw. Für diese Fälle fehlt Python 2.5 und darunter ein "eingebauter" eleganter Ansatz, und mehrere große Frameworks haben ihre eigenen Lösungen entwickelt (Zope, Trac, Twister). Python 2.6 und höher hat ABC-Klassen, um das zu lösen.


7
2018-06-20 03:31



In C ++ / Java / etc wird Polymorphie durch Vererbung verursacht. Gebt diesen fehlgeleiteten Glauben auf, und dynamische Sprachen öffnen sich für euch.

Im Wesentlichen gibt es in Python keine Schnittstelle, sondern "das Verständnis, dass bestimmte Methoden aufrufbar sind". Ziemlich hand-wellig und akademisch klingend, nein? Es bedeutet, dass Sie, weil Sie "sprechen" aufrufen, klar erwarten, dass das Objekt eine "sprechen" -Methode haben sollte. Einfach, nicht wahr? Dies ist sehr liskov-ian, da die Benutzer einer Klasse ihre Schnittstelle definieren, ein gutes Design-Konzept, das Sie in gesündere TDD führt.

Was übrig bleibt, ist, wie es einem anderen Poster höflicherweise gelungen ist, zu vermeiden, ein Code-Sharing-Trick. Sie könnten dasselbe Verhalten in jede "Kind" -Klasse schreiben, aber das wäre überflüssig. Leichtere Erben- oder Einfügungsfunktionalität, die in der Vererbungshierarchie invariant ist. Kleiner, DRY-er-Code ist im Allgemeinen besser.


5
2018-06-20 01:01



Es ist keine Vererbung, die Duck-Typing sinnlos macht, es sind Interfaces - wie die, die Sie beim Erstellen einer abstrakten Tierklasse gewählt haben.

Wenn Sie eine Tierklasse verwendet hätten, die ihren Nachkommen ein echtes Verhalten vorschreibt, dann wären Hunde- und Katzenklassen, die etwas zusätzliches Verhalten eingeführt haben, ein Grund für beide Klassen. Nur wenn die Vorfahrklasse den Nachkommenklassen keinen tatsächlichen Code beifügt, ist Ihr Argument korrekt.

Da Python die Fähigkeiten jedes Objekts direkt kennen kann und diese Fähigkeiten über die Klassendefinition hinaus veränderbar sind, ist die Idee, eine reine abstrakte Schnittstelle zu verwenden, um dem Programm zu "sagen", welche Methoden aufgerufen werden können, etwas sinnlos. Aber das ist nicht der einzige oder sogar der Hauptpunkt der Vererbung.


5
2017-10-15 22:28



Sie können die Vererbung in Python und so ziemlich jeder anderen Sprache umgehen. Es geht jedoch um Code-Wiederverwendung und Code-Vereinfachung.

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 es können.

Angenommen, du hast einen Hund, der das Tier unterklassifiziert hat.

command = raw_input("What do you want the dog to do?")
if command in dir(d): getattr(d,command)()

Wenn der eingegebene Benutzer verfügbar ist, wird der Code die richtige Methode ausführen.

Mit dieser können Sie eine beliebige Kombination von Säugetier / Reptil / Vogel Hybrid-Monstrosität erstellen, die Sie wollen, und jetzt können Sie es sagen "Bark!" während du fliegst und seine gegabelte Zunge herausstehst, und sie wird richtig damit umgehen! Viel Spass damit!


1
2018-06-24 23:40



Ich sehe nicht viel Sinn in der Vererbung.

Jedes Mal, wenn ich jemals Vererbung in realen Systemen verwendet habe, wurde ich verbrannt, weil es zu einem Wirrwarr von Abhängigkeiten führte, oder ich erkannte einfach rechtzeitig, dass es ohne mich besser gelingen würde. Jetzt vermeide ich es so viel wie möglich. Ich habe einfach nie einen Nutzen dafür.

class Repeat:
    "Send a message more than once"
    def __init__(repeat, times, do):
        repeat.times = times
        repeat.do = do

    def __call__(repeat):
        for i in xrange(repeat.times):
             repeat.do()

class Speak:
    def __init__(speak, animal):
        """
        Check that the animal can speak.

        If not we can do something about it (e.g. ignore it).
        """
        speak.__call__ = animal.speak

    def twice(speak):
        Repeat(2, speak)()

class Dog:
     def speak(dog):
         print "Woof"

class Cat:
     def speak(cat):
         print "Meow"

>>> felix = Cat()
>>> Speak(felix)()
Meow

>>> fido = Dog()
>>> speak = Speak(fido)
>>> speak()
Woof

>>> speak.twice()
Woof

>>> speak_twice = Repeat(2, Speak(felix))
>>> speak_twice()
Meow
Meow

James Gosling wurde einmal auf einer Pressekonferenz gefragt: "Wenn du zurückgehen und Java anders machen könntest, was würdest du weglassen?". Seine Antwort war "Klassen", zu denen es Gelächter gab. Er meinte es ernst und erklärte, dass es nicht das Problem sei, sondern das Erbe.

Ich sehe es wie eine Drogenabhängigkeit - es gibt dir eine schnelle Lösung, die sich gut anfühlt, aber am Ende vermasselt es dich. Damit meine ich, dass es eine bequeme Möglichkeit ist, Code wiederzuverwenden, aber es zwingt eine ungesunde Verbindung zwischen Kind- und Elternklasse. Änderungen am übergeordneten Element können das untergeordnete Element beschädigen. Das Kind ist für bestimmte Funktionen vom Elternteil 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 zusammengesetzt werden. Durch die Verwendung von richtig gestalteten Schnittstellen können alle Kopplungen eliminiert werden und wir bieten eine sehr gut zusammensetzbare API (Das ist nichts Neues - die meisten Programmierer machen das schon, einfach nicht genug). Beachten Sie, dass die implementierende Klasse die Funktionalität nicht einfach offen legen darf, ansonsten sollte der Client nur die komponierten Klassen direkt verwenden Muss mach etwas Neues, indem du diese Funktionalität kombinierst.

Es gibt das Argument aus dem Erbschaftslager, dass reine Delegationsimplementierungen leiden, weil sie viele "Leim" -Methoden benötigen, die einfach Werte über eine Delegationskette weitergeben. Dies bedeutet jedoch lediglich, dass ein vererbungsähnliches Design mit Delegierung neu erfunden wird. Programmierer, die sich zu lange mit vererbungsbasierten Designs beschäftigt haben, sind besonders anfällig dafür, in diese Falle zu geraten, da sie, ohne es zu merken, darüber nachdenken, wie sie etwas durch Vererbung implementieren und dann in Delegation umwandeln.

Die richtige Trennung von Belangen wie dem obigen Code erfordert keine Leimmethoden, da jeder Schritt tatsächlich erfolgt den Wert erhöhenalso sind sie überhaupt keine "Klebstoff" -Methoden (wenn sie keinen Mehrwert ergeben, ist das Design fehlerhaft).

Es läuft darauf hinaus:

  • Für wiederverwendbaren Code sollte jede Klasse nur eine Sache tun (und mach es gut).

  • Vererbung erstellt Klassen, die dies tun mehr als eine Sache, weil sie es sind mit Elternklassen verwechselt.

  • Daher macht die Verwendung der Vererbung Klassen, die schwer wiederzuverwenden sind.


1
2017-07-01 20:55