Was ist der richtige Weg, wenn Sie die Elternklasse __init__ mit Mehrfachvererbung aufrufen?

174

Angenommen, ich habe ein Szenario mit Mehrfachvererbung:

class A(object):
    # code for A here

class B(object):
    # code for B here

class C(A, B):
    def __init__(self):
        # What's the right code to write here to ensure 
        # A.__init__ and B.__init__ get called?

Es gibt zwei typische Ansätze zum Schreiben C‚s __init__:

  1. (alter Stil) ParentClass.__init__(self)
  2. (neuerer Stil) super(DerivedClass, self).__init__()

In beiden Fällen funktioniert der Code jedoch nicht richtig , wenn die übergeordneten Klassen ( Aund B) nicht derselben Konvention folgen (einige werden möglicherweise übersehen oder mehrmals aufgerufen).

Also, was ist wieder der richtige Weg? Es ist leicht zu sagen, "sei einfach konsequent, folge dem einen oder anderen", aber wenn Aoder Baus einer Bibliothek eines Drittanbieters, was dann? Gibt es einen Ansatz, der sicherstellen kann, dass alle übergeordneten Klassenkonstruktoren aufgerufen werden (und zwar in der richtigen Reihenfolge und nur einmal)?

Bearbeiten: um zu sehen, was ich meine, wenn ich tue:

class A(object):
    def __init__(self):
        print("Entering A")
        super(A, self).__init__()
        print("Leaving A")

class B(object):
    def __init__(self):
        print("Entering B")
        super(B, self).__init__()
        print("Leaving B")

class C(A, B):
    def __init__(self):
        print("Entering C")
        A.__init__(self)
        B.__init__(self)
        print("Leaving C")

Dann bekomme ich:

Entering C
Entering A
Entering B
Leaving B
Leaving A
Entering B
Leaving B
Leaving C

Beachten Sie, dass Bder Init zweimal aufgerufen wird. Wenn ich mache:

class A(object):
    def __init__(self):
        print("Entering A")
        print("Leaving A")

class B(object):
    def __init__(self):
        print("Entering B")
        super(B, self).__init__()
        print("Leaving B")

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

Dann bekomme ich:

Entering C
Entering A
Leaving A
Leaving C

Beachten Sie, dass Bder Init nie aufgerufen wird. Es scheint also, dass ich keine sichere Wahl für die Klasse treffen kann, die ich schreibe ( ) , wenn ich nicht die Initialen der Klassen kenne / kontrolliere, von denen ich erbe ( Aund ).BC

Adam Parkin
quelle

Antworten:

78

Beide Wege funktionieren gut. Der verwendete Ansatz super()führt zu einer größeren Flexibilität für Unterklassen.

Im Direktanruf-Ansatz C.__init__können sowohl A.__init__als auch angerufen werden B.__init__.

Bei der Verwendung super()müssen die Klassen für eine kooperative Mehrfachvererbung ausgelegt sein, bei der CAufrufe superden ACode aufrufen, superder auch den BCode aufruft . Weitere Informationen dazu, was getan werden kann, finden Sie unter http://rhettinger.wordpress.com/2011/05/26/super-considered-supersuper .

[Antwortfrage wie später bearbeitet]

Es scheint also, dass ich keine sichere Wahl für die Klasse treffen kann, die ich schreibe (C), wenn ich nicht die Initialen der Klassen kenne / kontrolliere, von denen ich erbe (A und B).

Der Artikel, auf den verwiesen wird, zeigt, wie Sie mit dieser Situation umgehen können, indem Sie eine Wrapper-Klasse um Aund hinzufügen B. Im Abschnitt "Einbindung einer nicht kooperativen Klasse" finden Sie ein ausgearbeitetes Beispiel.

Man könnte sich wünschen, dass die Mehrfachvererbung einfacher wäre und Sie mühelos Auto- und Flugzeugklassen zusammenstellen könnten, um ein FlyingCar zu erhalten. In der Realität benötigen jedoch separat gestaltete Komponenten häufig Adapter oder Wrapper, bevor sie so nahtlos zusammenpassen, wie wir möchten :-)

Ein anderer Gedanke: Wenn Sie mit dem Erstellen von Funktionen mit Mehrfachvererbung nicht zufrieden sind, können Sie die Komposition verwenden, um vollständig zu steuern, welche Methoden bei welchen Gelegenheiten aufgerufen werden.

Raymond Hettinger
quelle
4
Nein, das tun sie nicht. Wenn B's init nicht super aufruft, wird B's init nicht aufgerufen, wenn wir den super().__init__()Ansatz machen. Wenn ich anrufen A.__init__()und B.__init__()direkt, dann (wenn A und B Telefonieren super) erhalte ich B init mehrmals aufgerufen werden.
Adam Parkin
3
@AdamParkin ( in Bezug auf Ihre Frage so editierten): Wenn einer der übergeordneten Klassen für die Verwendung ist nicht mit super () , kann es in der Regel in einer Weise gewickelt werden, die hinzufügt Super Anruf. Der Artikel, auf den verwiesen wird, zeigt ein ausgearbeitetes Beispiel im Abschnitt "Einbindung einer nicht kooperativen Klasse".
Raymond Hettinger
1
Irgendwie habe ich diesen Abschnitt verpasst, als ich den Artikel gelesen habe. Genau das, wonach ich gesucht habe. Vielen Dank!
Adam Parkin
1
Wenn Sie Python schreiben (hoffentlich 3!) Und Vererbung jeglicher Art verwenden, insbesondere aber mehrere, sollte rhettinger.wordpress.com/2011/05/26/super-considered-super gelesen werden müssen.
Shawn Mehan
1
Upvoting, weil wir endlich wissen, warum wir keine fliegenden Autos haben, als wir uns sicher waren, dass wir es jetzt haben würden.
msouth
64

Die Antwort auf Ihre Frage hängt von einem sehr wichtigen Aspekt ab: Sind Ihre Basisklassen für Mehrfachvererbung ausgelegt?

Es gibt 3 verschiedene Szenarien:

  1. Die Basisklassen sind unabhängige, eigenständige Klassen.

    Wenn Ihre Basisklassen separate Entitäten sind, die unabhängig voneinander funktionieren können und sich nicht kennen, sind sie nicht für die Mehrfachvererbung ausgelegt. Beispiel:

    class Foo:
        def __init__(self):
            self.foo = 'foo'
    
    class Bar:
        def __init__(self, bar):
            self.bar = bar

    Wichtig: Beachten Sie, dass weder Foonoch BarAnrufe super().__init__()! Aus diesem Grund hat Ihr Code nicht richtig funktioniert. Aufgrund der Funktionsweise der Diamantvererbung in Python sollten Klassen, deren Basisklasse ist, objectnicht aufgerufen werdensuper().__init__() . Wie Sie bemerkt haben, würde dies die Mehrfachvererbung unterbrechen, da Sie am Ende __init__eher eine andere Klasse als eine anrufen object.__init__(). ( Haftungsausschluss: Das Vermeiden super().__init__()von objectUnterklassen ist meine persönliche Empfehlung und keineswegs ein vereinbarter Konsens in der Python-Community. Einige Leute bevorzugen die Verwendung superin jeder Klasse und argumentieren, dass Sie immer einen Adapter schreiben können, wenn sich die Klasse nicht so verhält du erwartest.)

    Dies bedeutet auch, dass Sie niemals eine Klasse schreiben sollten, die von objecteiner __init__Methode erbt und keine hat . Das Nichtdefinieren einer __init__Methode hat den gleichen Effekt wie das Aufrufensuper().__init__() . Wenn Ihre Klasse direkt von erbt object, stellen Sie sicher, dass Sie einen leeren Konstruktor wie folgt hinzufügen:

    class Base(object):
        def __init__(self):
            pass

    In dieser Situation müssen Sie jeden übergeordneten Konstruktor manuell aufrufen. Es gibt zwei Möglichkeiten, dies zu tun:

    • Ohne super

      class FooBar(Foo, Bar):
          def __init__(self, bar='bar'):
              Foo.__init__(self)  # explicit calls without super
              Bar.__init__(self, bar)
    • Mit super

      class FooBar(Foo, Bar):
          def __init__(self, bar='bar'):
              super().__init__()  # this calls all constructors up to Foo
              super(Foo, self).__init__(bar)  # this calls all constructors after Foo up
                                              # to Bar

    Jede dieser beiden Methoden hat ihre eigenen Vor- und Nachteile. Wenn Sie verwenden super, unterstützt Ihre Klasse die Abhängigkeitsinjektion . Andererseits ist es einfacher, Fehler zu machen. Wenn Sie beispielsweise die Reihenfolge von Foound Bar(wie class FooBar(Bar, Foo)) ändern , müssen Sie die superAnrufe entsprechend aktualisieren . Ohne dass superSie sich darüber keine Sorgen machen müssen und der Code viel besser lesbar ist.

  2. Eine der Klassen ist ein Mixin.

    Ein Mixin ist eine Klasse, die entworfen wurde , mit Mehrfachvererbung verwendet werden soll. Dies bedeutet, dass wir nicht beide übergeordneten Konstruktoren manuell aufrufen müssen, da das Mixin automatisch den 2. Konstruktor für uns aufruft. Da wir diesmal nur einen einzigen Konstruktor aufrufen müssen, können wir dies tun, um superzu vermeiden, dass der Name der übergeordneten Klasse fest codiert werden muss.

    Beispiel:

    class FooMixin:
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)  # forwards all unused arguments
            self.foo = 'foo'
    
    class Bar:
        def __init__(self, bar):
            self.bar = bar
    
    class FooBar(FooMixin, Bar):
        def __init__(self, bar='bar'):
            super().__init__(bar)  # a single call is enough to invoke
                                   # all parent constructors
    
            # NOTE: `FooMixin.__init__(self, bar)` would also work, but isn't
            # recommended because we don't want to hard-code the parent class.

    Die wichtigen Details hier sind:

    • Das Mixin ruft an super().__init__() alle empfangenen Argumente auf und durchläuft sie.
    • Die Unterklasse erbt von der mixin zuerst : class FooBar(FooMixin, Bar). Wenn die Reihenfolge der Basisklassen falsch ist, wird der Konstruktor des Mixins niemals aufgerufen.
  3. Alle Basisklassen sind für die kooperative Vererbung ausgelegt.

    Klassen, die für die kooperative Vererbung entwickelt wurden, ähneln Mixins: Sie leiten alle nicht verwendeten Argumente an die nächste Klasse weiter. Wie zuvor müssen wir nur anrufensuper().__init__() und alle übergeordneten Konstruktoren werden als Kettenaufruf bezeichnet.

    Beispiel:

    class CoopFoo:
        def __init__(self, **kwargs):
            super().__init__(**kwargs)  # forwards all unused arguments
            self.foo = 'foo'
    
    class CoopBar:
        def __init__(self, bar, **kwargs):
            super().__init__(**kwargs)  # forwards all unused arguments
            self.bar = bar
    
    class CoopFooBar(CoopFoo, CoopBar):
        def __init__(self, bar='bar'):
            super().__init__(bar=bar)  # pass all arguments on as keyword
                                       # arguments to avoid problems with
                                       # positional arguments and the order
                                       # of the parent classes

    In diesem Fall spielt die Reihenfolge der übergeordneten Klassen keine Rolle. Wir könnten genauso gut von erbenCoopBar zuerst , und der Code würde immer noch genauso funktionieren. Dies gilt jedoch nur, da alle Argumente als Schlüsselwortargumente übergeben werden. Die Verwendung von Positionsargumenten würde es leicht machen, die Reihenfolge der Argumente falsch zu bestimmen. Daher ist es üblich, dass kooperative Klassen nur Schlüsselwortargumente akzeptieren.

    Dies ist auch eine Ausnahme von der Regel, die ich zuvor erwähnt habe: Beide CoopFoound CoopBarerben von object, aber sie rufen immer noch auf super().__init__(). Andernfalls würde es keine kooperative Vererbung geben.

Fazit: Die richtige Implementierung hängt von den Klassen ab, von denen Sie erben.

Der Konstruktor ist Teil der öffentlichen Schnittstelle einer Klasse. Wenn die Klasse als Mixin oder zur kooperativen Vererbung konzipiert ist, muss dies dokumentiert werden. Wenn in den Dokumenten nichts dergleichen erwähnt wird, kann davon ausgegangen werden, dass die Klasse nicht für die kooperative Mehrfachvererbung ausgelegt ist.

Aran-Fey
quelle
2
Dein zweiter Punkt hat mich umgehauen. Ich habe Mixins immer nur rechts von der eigentlichen Superklasse gesehen und fand sie ziemlich locker und gefährlich, da Sie nicht überprüfen können, ob die Klasse, in die Sie mischen, die Attribute hat, die Sie erwarten. Ich habe nie daran gedacht, einen General super().__init__(*args, **kwargs)in das Mixin aufzunehmen und es zuerst zu schreiben. Es macht so viel Sinn.
Minix
10

Jeder Ansatz ("neuer Stil" oder "alter Stil") funktioniert, wenn Sie die Kontrolle über den Quellcode für Aund habenB . Andernfalls kann die Verwendung einer Adapterklasse erforderlich sein.

Quellcode zugänglich: Richtige Verwendung des "neuen Stils"

class A(object):
    def __init__(self):
        print("-> A")
        super(A, self).__init__()
        print("<- A")

class B(object):
    def __init__(self):
        print("-> B")
        super(B, self).__init__()
        print("<- B")

class C(A, B):
    def __init__(self):
        print("-> C")
        # Use super here, instead of explicit calls to __init__
        super(C, self).__init__()
        print("<- C")
>>> C()
-> C
-> A
-> B
<- B
<- A
<- C

Hier bestimmt die Methodenauflösungsreihenfolge (MRO) Folgendes:

  • C(A, B)diktiert Adann zuerst B. MRO istC -> A -> B -> object .
  • super(A, self).__init__()weiter entlang der MRO - Kette in eingeleitet C.__init__zu B.__init__.
  • super(B, self).__init__()weiter entlang der MRO - Kette in eingeleitet C.__init__zu object.__init__.

Man könnte sagen, dass dieser Fall für die Mehrfachvererbung ausgelegt ist .

Quellcode zugänglich: Richtige Verwendung des "alten Stils"

class A(object):
    def __init__(self):
        print("-> A")
        print("<- A")

class B(object):
    def __init__(self):
        print("-> B")
        # Don't use super here.
        print("<- B")

class C(A, B):
    def __init__(self):
        print("-> C")
        A.__init__(self)
        B.__init__(self)
        print("<- C")
>>> C()
-> C
-> A
<- A
-> B
<- B
<- C

Hier spielt MRO keine Rolle, da A.__init__und B.__init__explizit aufgerufen werden. class C(B, A):würde genauso gut funktionieren.

Obwohl dieser Fall im neuen Stil nicht wie der vorherige für die Mehrfachvererbung "ausgelegt" ist, ist eine Mehrfachvererbung weiterhin möglich.


Was ist nun, wenn Aund Baus einer Drittanbieter-Bibliothek stammen - dh Sie haben keine Kontrolle über den Quellcode für AundB ? Die kurze Antwort: Sie müssen eine Adapterklasse entwerfen, die die erforderlichen superAufrufe implementiert , und dann eine leere Klasse verwenden, um die MRO zu definieren (siehe Raymond Hettingers Artikel übersuper - insbesondere den Abschnitt "Einbinden einer nicht kooperativen Klasse").

Eltern von Drittanbietern: Anicht implementiert super; Btut

class A(object):
    def __init__(self):
        print("-> A")
        print("<- A")

class B(object):
    def __init__(self):
        print("-> B")
        super(B, self).__init__()
        print("<- B")

class Adapter(object):
    def __init__(self):
        print("-> C")
        A.__init__(self)
        super(Adapter, self).__init__()
        print("<- C")

class C(Adapter, B):
    pass
>>> C()
-> C
-> A
<- A
-> B
<- B
<- C

Klasse Adapterimplementiert, superso dass Cdie MRO definiert werden kann, die bei der super(Adapter, self).__init__()Ausführung ins Spiel kommt.

Und wenn es umgekehrt ist?

Eltern von Drittanbietern: AGeräte super; Bnicht

class A(object):
    def __init__(self):
        print("-> A")
        super(A, self).__init__()
        print("<- A")

class B(object):
    def __init__(self):
        print("-> B")
        print("<- B")

class Adapter(object):
    def __init__(self):
        print("-> C")
        super(Adapter, self).__init__()
        B.__init__(self)
        print("<- C")

class C(Adapter, A):
    pass
>>> C()
-> C
-> A
<- A
-> B
<- B
<- C

Gleiches Muster hier, außer dass die Ausführungsreihenfolge eingeschaltet ist Adapter.__init__; superzuerst anrufen, dann explizit anrufen. Beachten Sie, dass für jeden Fall mit Eltern von Drittanbietern eine eindeutige Adapterklasse erforderlich ist.

Es scheint also, dass ich keine sichere Wahl für die Klasse treffen kann, die ich schreibe ( ) , wenn ich nicht die Initialen der Klassen kenne / kontrolliere, von denen ich erbe ( Aund ).BC

Obwohl Sie die Fälle behandeln können, in denen Sie den Quellcode von und mithilfe einer Adapterklasse nicht steuern , müssen Sie wissen, wie die Inits der übergeordneten Klassen (wenn überhaupt) implementiert werden, um dies zu tun.ABsuper

Nathaniel Jones
quelle
4

Wie Raymond in seiner Antwort sagte, ein direkter Anruf bei A.__init__und B.__init__funktioniert gut, und Ihr Code wäre lesbar.

Die Vererbungsverknüpfung zwischen Cund diesen Klassen wird jedoch nicht verwendet . Wenn Sie diesen Link nutzen, erhalten Sie mehr Konsistenz und können eventuelle Refactorings einfacher und weniger fehleranfällig durchführen. Ein Beispiel dafür:

class C(A, B):
    def __init__(self):
        print("entering c")
        for base_class in C.__bases__:  # (A, B)
             base_class.__init__(self)
        print("leaving c")
Jundiaius
quelle
1
Die beste Antwort imho. Fand dies besonders hilfreich, da es zukunftssicherer ist
Stephen Ellwood
3

Dieser Artikel erklärt die kooperative Mehrfachvererbung:

http://www.artima.com/weblogs/viewpost.jsp?thread=281127

Es wird die nützliche Methode erwähnt, die mro()Ihnen die Reihenfolge der Methodenauflösung zeigt. In Ihrem zweiten Beispiel, wo Sie rufen superin Ader superweiterhin Anruf in MRO. Die nächste Klasse in der Reihenfolge ist B, deshalbB der Init zum ersten Mal aufgerufen wird.

Hier ist ein technischerer Artikel von der offiziellen Python-Site:

http://www.python.org/download/releases/2.3/mro/

Kelvin
quelle
2

Wenn Sie Unterklassenklassen aus Bibliotheken von Drittanbietern multiplizieren, gibt es keinen blinden Ansatz zum Aufrufen der Basisklassenmethoden __init__(oder anderer Methoden), der tatsächlich funktioniert, unabhängig davon, wie die Basisklassen programmiert sind.

supermacht es möglich , zu Schreibklassen entwickelt , um gemeinsam Methoden im Rahmen von komplexen Mehrfachvererbung Bäume zu implementieren , die zu der Klasse Autor muss nicht bekannt sein. Es gibt jedoch keine Möglichkeit, damit korrekt von beliebigen Klassen zu erben, die möglicherweise verwendet werden oder nicht super.

Ob eine Klasse mit superoder mit direkten Aufrufen der Basisklasse für eine Unterklasse ausgelegt ist, ist im Wesentlichen eine Eigenschaft, die Teil der "öffentlichen Schnittstelle" der Klasse ist und als solche dokumentiert werden sollte. Wenn Sie Bibliotheken von Drittanbietern so verwenden, wie es der Bibliotheksautor erwartet hat, und die Bibliothek über eine angemessene Dokumentation verfügt, werden Sie normalerweise darüber informiert, was Sie tun müssen, um bestimmte Dinge in Unterklassen zu unterteilen. Wenn nicht, müssen Sie sich den Quellcode der Klassen ansehen, die Sie unterklassifizieren, und herausfinden, wie ihre Konvention zum Aufrufen von Basisklassen lautet. Wenn Sie mehrere Klassen aus einer oder mehreren Bibliotheken von Drittanbietern auf eine Weise kombinieren, die die Autoren der Bibliothek nicht erwartet hatten, ist es möglicherweise nicht möglich, Methoden der Superklasse konsistent aufzurufen Superklasse überhaupt;; Wenn Klasse A Teil einer Hierarchie ist, die verwendet, superund Klasse B Teil einer Hierarchie ist, die Super nicht verwendet, funktioniert garantiert keine der beiden Optionen. Sie müssen eine Strategie finden, die für jeden Einzelfall funktioniert.

Ben
quelle
@RaymondHettinger Nun, Sie haben bereits einen Artikel mit einigen Gedanken dazu in Ihrer Antwort geschrieben und verlinkt. Ich glaube, ich habe dem nicht viel hinzuzufügen. :) Ich glaube nicht, dass es möglich ist, eine nicht super-verwendende Klasse generisch an eine Super-Hierarchie anzupassen. Sie müssen eine Lösung finden, die auf die jeweiligen Klassen zugeschnitten ist.
Ben