Chiamare la classe genitore __init__ con ereditarietà multipla, qual è la strada giusta?


175

Supponiamo che io abbia uno scenario di ereditarietà multipla:

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?

Ci sono due approcci tipici di scrittura C's __init__:

  1. (vecchio stile) ParentClass.__init__(self)
  2. (Stile più recente) super(DerivedClass, self).__init__()

Tuttavia, in entrambi i casi, se le classi parent ( Ae B) non seguono la stessa convenzione, il codice non funzionerà correttamente (alcuni potrebbero non essere rilevati o essere chiamati più volte).

Quindi qual è di nuovo il modo corretto? È facile dire "basta essere coerenti, seguire l'uno o l'altro", ma se Ao Bprovengono da una libreria di terze parti, che cosa succede? Esiste un approccio che può garantire che tutti i costruttori della classe genitore vengano chiamati (e nell'ordine corretto e solo una volta)?

Modifica: per vedere cosa intendo, se lo faccio:

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")

Quindi ottengo:

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

Si noti che Binit viene chiamato due volte. Se lo faccio:

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")

Quindi ottengo:

Entering C
Entering A
Leaving A
Leaving C

Nota che Binit non viene mai chiamato. Quindi sembra che se non conosco / controllo gli init delle classi che eredito da ( Ae B) non posso fare una scelta sicura per la classe che sto scrivendo ( C).


Risposte:


78

Entrambi i modi funzionano bene. L'approccio utilizzato super()porta a una maggiore flessibilità per le sottoclassi.

Nell'approccio di chiamata diretta, è C.__init__possibile chiamare sia A.__init__e B.__init__.

Quando si usano super(), le classi devono essere progettate per l'ereditarietà multipla cooperativa dove Cchiama super, che invoca Ail codice che chiamerà anche superche invoca Bil codice. Vedi http://rhettinger.wordpress.com/2011/05/26/super-considered-super per maggiori dettagli su cosa si può fare super.

[Domanda di risposta modificata in seguito]

Quindi sembra che se non conosco / controllo gli init delle classi che eredito da (A e B) non posso fare una scelta sicura per la classe che sto scrivendo (C).

L'articolo di riferimento mostra come gestire questa situazione aggiungendo una classe wrapper Ae B. C'è un esempio elaborato nella sezione intitolata "Come incorporare una classe non cooperativa".

Si potrebbe desiderare che l'ereditarietà multipla sia più semplice, permettendoti di comporre senza fatica le classi Car e Airplane per ottenere una FlyingCar, ma la realtà è che i componenti progettati separatamente necessitano spesso di adattatori o involucri prima di essere montati insieme senza problemi come vorremmo :-)

Un altro pensiero: se non sei soddisfatto della funzionalità di composizione usando l'ereditarietà multipla, puoi usare la composizione per un controllo completo su quali metodi vengono chiamati in quali occasioni.


4
No, non lo fanno. Se l'init di B non chiama super, allora l'init di B non verrà chiamato se facciamo l' super().__init__()approccio. Se chiamo A.__init__()e B.__init__()direttamente, allora (se A e B lo chiamano super) ricevo la chiamata di B più volte.
Adam Parkin,

3
@AdamParkin (riguardo alla tua domanda modificata): se una delle classi principali non è progettata per l'uso con super () , di solito può essere racchiusa in un modo che aggiunge la super chiamata. L'articolo di riferimento mostra un esempio elaborato nella sezione intitolata "Come incorporare una classe non cooperativa".
Raymond Hettinger,

1
In qualche modo sono riuscito a perdere quella sezione quando ho letto l'articolo. Esattamente quello che stavo cercando. Grazie!
Adam Parkin,

1
Se stai scrivendo python (si spera 3!) E stai usando eredità di qualsiasi tipo, ma soprattutto multiple, allora dovrebbe essere richiesta la lettura di rhettinger.wordpress.com/2011/05/26/super-considered-super .
Shawn Mehan,

1
Voto perché finalmente sappiamo perché non abbiamo macchine volanti quando eravamo sicuri che avremmo ormai.
msouth,

66

La risposta alla tua domanda dipende da un aspetto molto importante: le tue classi di base sono progettate per l'ereditarietà multipla?

Esistono 3 diversi scenari:

  1. Le classi di base sono classi indipendenti, indipendenti.

    Se i corsi di base sono entità separate che sono in grado di funzionare in modo indipendente e non sanno l'un l'altro, stanno non progettati per l'ereditarietà multipla. Esempio:

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

    Importante: notare che né FooBarchiamate super().__init__()! Ecco perché il tuo codice non ha funzionato correttamente. A causa del modo in cui l'ereditarietà dei diamanti funziona in Python, le classi la cui classe base è objectnon dovrebbero chiamaresuper().__init__() . Come avrai notato, farlo spezzerebbe l'eredità multipla perché finisci per chiamare un'altra classe __init__anziché object.__init__(). ( Dichiarazione di non responsabilità: evitare super().__init__()in object-subclasses è la mia raccomandazione personale e non è affatto un consenso concordato nella comunità python. Alcune persone preferiscono usare superin ogni classe, sostenendo che puoi sempre scrivere un adattatore se la classe non si comporta come ti aspetti.)

    Questo significa anche che non dovresti mai scrivere una classe che eredita objecte non ha un __init__metodo. Non definire affatto un __init__metodo ha lo stesso effetto della chiamata super().__init__(). Se la tua classe eredita direttamente da object, assicurati di aggiungere un costruttore vuoto in questo modo:

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

    Ad ogni modo, in questa situazione, dovrai chiamare manualmente ciascun costruttore principale. Esistono due modi per farlo:

    • Senza super

      class FooBar(Foo, Bar):
          def __init__(self, bar='bar'):
              Foo.__init__(self)  # explicit calls without super
              Bar.__init__(self, bar)
    • Con 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

    Ognuno di questi due metodi ha i suoi vantaggi e svantaggi. Se usi super, la tua classe supporterà l' iniezione di dipendenza . D'altra parte, è più facile commettere errori. Ad esempio, se cambi l'ordine Fooe Bar(come class FooBar(Bar, Foo)), dovresti aggiornare le superchiamate in modo che corrispondano. Senza di superte non devi preoccuparti di questo, e il codice è molto più leggibile.

  2. Una delle lezioni è un mixin.

    Un mixin è una classe progettata per essere utilizzata con ereditarietà multipla. Ciò significa che non è necessario chiamare manualmente entrambi i costruttori principali, poiché il mixin chiamerà automaticamente il secondo costruttore per noi. Dato che questa volta dobbiamo chiamare un solo costruttore, possiamo farlo superper evitare di dover codificare il nome della classe genitore.

    Esempio:

    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.

    I dettagli importanti qui sono:

    • Il mixin chiama super().__init__()e passa attraverso tutti gli argomenti che riceve.
    • I eredita sottoclasse dalla mixin prima : class FooBar(FooMixin, Bar). Se l'ordine delle classi base è sbagliato, il costruttore del mixin non verrà mai chiamato.
  3. Tutte le classi di base sono progettate per l'ereditarietà cooperativa.

    Le classi progettate per l'ereditarietà cooperativa sono molto simili ai mixin: passano attraverso tutti gli argomenti inutilizzati alla classe successiva. Come prima, non ci resta che chiamare super().__init__()e tutti i costruttori principali verranno chiamati a catena.

    Esempio:

    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 questo caso, l'ordine delle classi principali non ha importanza. Potremmo anche ereditare dal CoopBarprimo, e il codice funzionerebbe comunque allo stesso modo. Ma questo è vero solo perché tutti gli argomenti vengono passati come argomenti di parole chiave. L'uso di argomenti posizionali renderebbe semplice sbagliare l'ordine degli argomenti, quindi è consuetudine che le classi cooperative accettino solo argomenti di parole chiave.

    Questa è anche un'eccezione alla regola che ho citato in precedenza: entrambi CoopFooed CoopBarereditano da object, ma continuano a chiamare super().__init__(). Se non lo facessero, non ci sarebbe eredità cooperativa.

In conclusione: l'implementazione corretta dipende dalle classi da cui erediti.

Il costruttore fa parte dell'interfaccia pubblica di una classe. Se la classe è progettata come mixin o per ereditarietà cooperativa, questo deve essere documentato. Se i documenti non menzionano nulla del genere, si può presumere che la classe non sia progettata per l'ereditarietà multipla cooperativa.


2
Il tuo secondo punto mi ha lasciato senza fiato. Ho sempre visto i Mixin alla destra della superclasse reale e ho pensato che fossero piuttosto sciolti e pericolosi, dal momento che non puoi controllare se la classe in cui stai mescolando ha gli attributi che ti aspetti che abbia. Non ho mai pensato di mettere un generale super().__init__(*args, **kwargs)nel mixin e di scriverlo prima. Ha molto senso.
Minix,

10

Entrambi gli approcci ("nuovo stile" o "vecchio stile") funzioneranno se si ha il controllo del codice sorgente per AeB . Altrimenti, potrebbe essere necessario l'uso di una classe adattatore.

Codice sorgente accessibile: uso corretto del "nuovo stile"

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

Qui, l'ordine di risoluzione del metodo (MRO) impone quanto segue:

  • C(A, B)Aprima impone , quindi B. MRO è C -> A -> B -> object.
  • super(A, self).__init__()continua lungo la catena MRO avviata C.__init__a B.__init__.
  • super(B, self).__init__()continua lungo la catena MRO avviata C.__init__a object.__init__.

Si potrebbe dire che questo caso è progettato per l'ereditarietà multipla .

Codice sorgente accessibile: uso corretto di "vecchio stile"

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

Qui, MRO non ha importanza, poiché A.__init__e B.__init__sono chiamati in modo esplicito. class C(B, A):funzionerebbe altrettanto bene.

Sebbene questo caso non sia "progettato" per l'ereditarietà multipla nel nuovo stile come era il precedente, l'ereditarietà multipla è ancora possibile.


Ora, cosa succede se Ae Bprovengono da una libreria di terze parti, ovvero non hai alcun controllo sul codice sorgente per AeB ? La risposta breve: è necessario progettare una classe adattatore che implementa le superchiamate necessarie , quindi utilizzare una classe vuota per definire l'MRO (vedere l'articolo di Raymond Hettinger susuper - in particolare la sezione "Come incorporare una classe non cooperativa").

Genitori di terze parti: Anon implementa super; Bfa

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

La classe Adapterimplementa in supermodo tale da Cpoter definire l'MRO, che entra in gioco quando super(Adapter, self).__init__()viene eseguita.

E se fosse il contrario?

Genitori di terze parti: Aattrezzi super; Bnon

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

Stesso modello qui, tranne per l'ordine di esecuzione attivato Adapter.__init__; superchiamare prima, quindi chiamata esplicita. Si noti che ogni caso con genitori di terze parti richiede una classe adattatore unica.

Quindi sembra che se non conosco / controllo gli init delle classi che eredito da ( Ae B) non posso fare una scelta sicura per la classe che sto scrivendo ( C).

Sebbene sia possibile gestire i casi in cui non si controlla il codice sorgente Ae Butilizzando una classe adattatore, è vero che è necessario sapere come implementano super(se non del tutto) gli init delle classi parent .


4

Come ha detto Raymond nella sua risposta, una chiamata diretta A.__init__e B.__init__funziona bene e il tuo codice sarebbe leggibile.

Tuttavia, non utilizza il collegamento di ereditarietà tra Ce tali classi. Sfruttare quel collegamento ti dà più coerenza e rende eventuali rifatturazioni più facili e meno soggette a errori. Un esempio di come farlo:

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")

1
La migliore risposta imho. Ho trovato questo particolarmente utile in quanto è più a prova di futuro
Stephen Ellwood,

3

Questo articolo aiuta a spiegare l'ereditarietà multipla cooperativa:

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

Menziona l'utile metodo mro()che mostra l'ordine di risoluzione del metodo. Nel suo secondo esempio, dove si chiama superin A, la superchiamata continua sul in MRO. La classe successiva nell'ordine è B, ecco perché Binit si chiama la prima volta.

Ecco un articolo più tecnico dal sito ufficiale di Python:

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


2

Se si moltiplicano le classi di sottoclassi da librerie di terze parti, allora no, non esiste un approccio cieco alla chiamata dei __init__metodi della classe base (o di qualsiasi altro metodo) che funzioni effettivamente indipendentemente da come sono programmate le classi base.

superrende possibile per le classi di scrittura destinati a realizzare cooperativamente metodi come parte di complessi più alberi di ereditarietà che non devono essere conosciuti all'autore classe. Ma non c'è modo di usarlo per ereditare correttamente da classi arbitrarie che possono o non possono usare super.

In sostanza, se una classe è progettata per essere sottoclassata usando supero con chiamate dirette alla classe base è una proprietà che fa parte dell '"interfaccia pubblica" della classe e deve essere documentata come tale. Se stai usando librerie di terze parti nel modo previsto dall'autore della biblioteca e la biblioteca ha una documentazione ragionevole, normalmente ti direbbe cosa devi fare per sottoclassare cose particolari. In caso contrario, dovrai esaminare il codice sorgente per le classi che stai classificando in sottoclasse e vedere qual è la loro convenzione di invocazione per classe base. Se stai combinando più classi da una o più librerie di terze parti in un modo che gli autori delle biblioteche non si aspettavano, allora potrebbe non essere possibile invocare in modo coerente metodi di superclasse; se la classe A fa parte di una gerarchia usando supere la classe B fa parte di una gerarchia che non usa super, allora nessuna delle due opzioni è garantita per funzionare. Dovrai capire una strategia che funziona per ogni caso particolare.


@RaymondHettinger Bene, hai già scritto e collegato a un articolo con alcuni pensieri a riguardo nella tua risposta, quindi non credo di avere molto da aggiungere a questo. :) Non penso che sia possibile adattare genericamente qualsiasi classe non superuso a una super gerarchia; devi trovare una soluzione su misura per le classi particolari coinvolte.
Ben
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.