Ist Pythons Vererbung ein Vererbungsstil oder ein Kompositionsstil?

10

Wie sieht die idiomatische Vererbung in Python aus, da Python eine Mehrfachvererbung zulässt?

In Sprachen mit einfacher Vererbung wie Java wird die Vererbung verwendet, wenn Sie sagen können, dass ein Objekt ein anderes Objekt "ist" und Sie Code zwischen den Objekten teilen möchten (vom übergeordneten Objekt zum untergeordneten Objekt). Zum Beispiel könnte man sagen, dass dies Dogein Animal:

public class Animal {...}
public class Dog extends Animal {...}

Da Python jedoch die Mehrfachvererbung unterstützt, können wir ein Objekt erstellen, indem wir viele andere Objekte zusammensetzen. Betrachten Sie das folgende Beispiel:

class UserService(object):
    def validate_credentials(self, username, password):
        # validate the user credentials are correct
        pass


class LoggingService(object):
    def log_error(self, error):
        # log an error
        pass


class User(UserService, LoggingService):
    def __init__(self, username, password):
        self.username = username
        self.password = password

    def authenticate(self):
        if not super().validate_credentials(self.username, self.password):
            super().log_error('Invalid credentials supplied')
            return False
         return True

Ist dies eine akzeptable oder gute Verwendung der Mehrfachvererbung in Python? Anstatt zu sagen, dass Vererbung ist, wenn ein Objekt ein anderes Objekt "ist", erstellen wir ein UserModell, das aus UserServiceund besteht LoggingService.

Die gesamte Logik für Datenbank- oder Netzwerkoperationen kann vom UserModell getrennt gehalten werden, indem sie in das UserServiceObjekt eingefügt werden, und die gesamte Logik für die Anmeldung im wird beibehalten LoggingService.

Ich sehe einige Probleme mit diesem Ansatz:

  • Erschafft dies ein Gottobjekt? Da Usererbt von oder besteht aus UserServiceund LoggingServicefolgt es wirklich dem Prinzip der Einzelverantwortung?
  • Um auf Methoden für ein übergeordnetes / next-in-line-Objekt zugreifen zu können (z. B. müssen UserService.validate_credentialswir verwenden super. Dies macht es etwas schwieriger zu erkennen, welches Objekt mit dieser Methode umgehen wird, und ist nicht so klar wie beispielsweise , instanziieren UserServiceund so etwas tunself.user_service.validate_credentials

Was wäre der pythonische Weg, um den obigen Code zu implementieren?

Iain
quelle

Antworten:

9

Ist Pythons Vererbung ein Vererbungsstil oder ein Kompositionsstil?

Python unterstützt beide Stile. Sie demonstrieren die Beziehung zwischen Zusammensetzung und Zusammensetzung, bei der ein Benutzer über Protokollierungsfunktionen aus einer Quelle und über die Überprüfung von Anmeldeinformationen aus einer anderen Quelle verfügt. Die LoggingServiceund UserServiceBasen sind Mixins: Sie bieten Funktionalität und sollen nicht von selbst instanziiert werden.

Durch das Erstellen von Mixins im Typ haben Sie einen Benutzer, der protokollieren kann, der jedoch seine eigene Instanziierungsfunktionalität hinzufügen muss.

Dies bedeutet nicht, dass man sich nicht an eine einzelne Vererbung halten kann. Python unterstützt das auch. Wenn Ihre Entwicklungsfähigkeit durch die Komplexität der Mehrfachvererbung beeinträchtigt wird, können Sie dies vermeiden, bis Sie sich wohler fühlen oder zu einem Punkt im Design kommen, an dem Sie glauben, dass sich der Kompromiss lohnt.

Erschafft dies ein Gottobjekt?

Die Protokollierung scheint etwas tangential zu sein - Python hat ein eigenes Protokollierungsmodul mit einem Protokollierungsobjekt, und die Konvention lautet, dass es einen Protokollierer pro Modul gibt.

Legen Sie das Protokollierungsmodul jedoch beiseite. Möglicherweise verstößt dies gegen die Einzelverantwortung, aber in Ihrem spezifischen Kontext ist es möglicherweise entscheidend, einen Benutzer zu definieren. Die Deskretisierung der Verantwortung kann kontrovers sein. Das umfassendere Prinzip ist jedoch, dass Python es dem Benutzer ermöglicht, die Entscheidung zu treffen.

Ist super weniger klar?

superDies ist nur erforderlich, wenn Sie innerhalb einer Funktion mit demselben Namen an ein übergeordnetes Element in der Method Resolution Order (MRO) delegieren müssen. Es wird empfohlen, einen Aufruf der Methode des Elternteils zu verwenden, anstatt ihn fest zu codieren. Aber wenn Sie die Eltern nicht hart codieren wollten, brauchen Sie sie nicht super.

In Ihrem Beispiel hier müssen Sie nur tun self.validate_credentials. selfist aus Ihrer Sicht nicht klarer. Beide folgen dem MRO. Ich würde einfach jeden verwenden, wo es angebracht ist.

Wenn Sie stattdessen angerufen hätten authenticate, validate_credentialsmüssten Sie superdas übergeordnete Element verwenden (oder fest codieren), um einen Rekursionsfehler zu vermeiden.

Vorschlag für einen alternativen Code

Unter der Annahme, dass die Semantik in Ordnung ist (wie beim Protokollieren), würde ich im Unterricht Folgendes tun User:

    def validate_credentials(self): # changed from "authenticate" to 
                                    # demonstrate need for super
        if not super().validate_credentials(self.username, self.password):
            # just use self on next call, no need for super:
            self.log_error('Invalid credentials supplied') 
            return False
        return True
Aaron Hall
quelle
1
Ich stimme dir nicht zu. Durch Vererbung wird immer eine Kopie der öffentlichen Schnittstelle einer Klasse in ihren Unterklassen erstellt. Dies ist keine "hat-eine" Beziehung. Dies ist schlicht und einfach eine Untertypisierung und daher für die beschriebene Anwendung ungeeignet.
Jules
@ Jules Womit sind Sie nicht einverstanden? Ich habe viele Dinge gesagt, die nachweisbar sind, und Schlussfolgerungen gezogen, die logisch folgen. Sie sind falsch, wenn Sie sagen: "Durch Vererbung wird immer eine Kopie der öffentlichen Schnittstelle einer Klasse in ihren Unterklassen erstellt." In Python gibt es keine Kopie - die Methoden werden dynamisch gemäß der C3-Algorithmus-Methodenauflösungsreihenfolge (MRO) nachgeschlagen.
Aaron Hall
1
Es geht nicht um spezifische Details der Funktionsweise der Implementierung, sondern darum, wie die öffentliche Schnittstelle der Klasse aussieht. Im Fall des Beispiels haben UserObjekte in ihren Schnittstellen nicht nur die in der UserKlasse definierten Elemente, sondern auch die in UserServiceund definierten LoggingService. Dies ist keine "has-a" -Beziehung, da die öffentliche Schnittstelle kopiert wird (allerdings nicht durch direktes Kopieren, sondern durch indirektes Nachschlagen der Schnittstellen der Oberklassen).
Jules
Has-a bedeutet Zusammensetzung. Mixins sind eine Form der Zusammensetzung. Die User-Klasse ist kein UserService oder LoggingService, verfügt jedoch über diese Funktionalität. Ich denke, Pythons Vererbung unterscheidet sich mehr von der von Java als Sie denken.
Aaron Hall
@AaronHall Sie vereinfachen zu stark (dies widerspricht Ihnen eher einer anderen Antwort , die ich zufällig gefunden habe). Unter dem Gesichtspunkt der Subtypbeziehung ist ein Benutzer sowohl ein UserService als auch ein LoggingService. Der Geist hier besteht nun darin, Funktionen so zusammenzustellen, dass ein Benutzer über solche und solche Funktionen verfügt. Mixins müssen im Allgemeinen nicht mit Mehrfachvererbung implementiert werden. Dies ist jedoch die übliche Vorgehensweise in Python.
Coredump
-1

Abgesehen von der Tatsache, dass mehrere Oberklassen zulässig sind, unterscheidet sich die Vererbung von Python nicht wesentlich von der von Java, dh Mitglieder einer Unterklasse sind auch Mitglieder jedes ihrer Supertypen [1]. Die Tatsache, dass Python die Ententypisierung verwendet, spielt ebenfalls keine Rolle: Ihre Unterklasse verfügt über alle Mitglieder ihrer Oberklassen und kann daher von jedem Code verwendet werden, der diese Unterklassen verwenden könnte. Die Tatsache, dass Mehrfachvererbung mithilfe von Komposition effektiv implementiert wird, ist ein roter Faden: Das automatisierte Kopieren der Eigenschaften einer Klasse in eine andere ist das Problem, und es spielt keine Rolle, ob Komposition verwendet wird oder nur auf magische Weise erraten wird, wie die Mitglieder angenommen werden zu arbeiten: sie zu haben ist falsch.

Ja, dies verstößt gegen die Einzelverantwortung, da Sie Ihren Objekten die Möglichkeit geben, Aktionen auszuführen, die nicht logisch Teil ihrer Aufgaben sind. Ja, es werden "Gott" -Objekte geschaffen, was im Wesentlichen eine andere Art ist, dasselbe zu sagen.

Beim Entwerfen objektorientierter Systeme in Python gilt dieselbe Maxime, die in Java-Entwurfsbüchern gepredigt wird: Komposition gegenüber Vererbung bevorzugen. Gleiches gilt für (die meisten [2]) anderen Systeme mit Mehrfachvererbung.

[1]: Man könnte das eine "Ist-Eine" -Beziehung nennen, obwohl ich den Begriff persönlich nicht mag, weil er die Idee der Modellierung der realen Welt nahe legt und die objektorientierte Modellierung nicht mit der realen Welt identisch ist.

[2]: Bei C ++ bin ich mir nicht so sicher. C ++ unterstützt "private Vererbung", bei der es sich im Wesentlichen um eine Komposition handelt, ohne dass ein Feldname angegeben werden muss, wenn Sie die öffentlichen Mitglieder der geerbten Klasse verwenden möchten. Die öffentliche Schnittstelle der Klasse ist davon überhaupt nicht betroffen. Ich benutze es nicht gern , aber ich sehe keinen guten Grund, es nicht zu tun .

Jules
quelle