Llamando a la clase padre __init__ con herencia múltiple, ¿cuál es la forma correcta?

175

Digamos que tengo un escenario de herencia múltiple:

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?

Hay dos enfoques típicos de la escritura C's __init__:

  1. (viejo estilo) ParentClass.__init__(self)
  2. (estilo más nuevo) super(DerivedClass, self).__init__()

Sin embargo, en cualquier caso, si las clases principales ( Ay B) no siguen la misma convención, entonces el código no funcionará correctamente (algunos pueden perderse o recibir llamadas varias veces).

Entonces, ¿cuál es la forma correcta de nuevo? Es fácil decir "solo sea coherente, siga uno u otro", pero si es Ao Bes de una biblioteca de terceros, ¿entonces qué? ¿Existe un enfoque que pueda garantizar que se llame a todos los constructores de clases principales (y en el orden correcto, y solo una vez)?

Editar: para ver a qué me refiero, si lo hago:

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

Entonces me sale:

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

Tenga en cuenta que Bel init se llama dos veces. Si lo hago:

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

Entonces me sale:

Entering C
Entering A
Leaving A
Leaving C

Tenga en cuenta que Bnunca se llama a init. Por lo tanto, parece que a menos que sepa / controle los inicios de las clases que heredo de ( Ay B) no puedo tomar una decisión segura para la clase que estoy escribiendo ( C).

Adam Parkin
fuente

Respuestas:

78

Ambas formas funcionan bien. El enfoque que utiliza super()conduce a una mayor flexibilidad para las subclases.

En el enfoque de llamada directa, C.__init__puede llamar a ambos A.__init__y B.__init__.

Cuando se usa super(), las clases deben diseñarse para la herencia cooperativa múltiple donde se Cllama super, que invoca Ael código que también superinvoca Bel código que invoca . Consulte http://rhettinger.wordpress.com/2011/05/26/super-considered-super para obtener más detalles sobre lo que se puede hacer super.

[Pregunta de respuesta como se editó más tarde]

Por lo tanto, parece que a menos que sepa / controle los inicios de las clases que heredo de (A y B), no puedo tomar una decisión segura para la clase que estoy escribiendo (C).

El artículo al que se hace referencia muestra cómo manejar esta situación agregando una clase de contenedor alrededor de Ay B. Hay un ejemplo resuelto en la sección titulada "Cómo incorporar una clase no cooperativa".

Uno desearía que la herencia múltiple fuera más fácil, permitiéndole componer sin esfuerzo las clases de automóviles y aviones para obtener un FlyingCar, pero la realidad es que los componentes diseñados por separado a menudo necesitan adaptadores o envolturas antes de encajar tan fácilmente como nos gustaría :-)

Otro pensamiento: si no está satisfecho con la funcionalidad de composición mediante herencia múltiple, puede usar la composición para tener un control completo sobre qué métodos se llaman en qué ocasiones.

Raymond Hettinger
fuente
44
No, ellos no. Si el init de B no llama super, entonces el init de B no se llamará si hacemos el super().__init__()enfoque. Si llamo A.__init__()y B.__init__()directamente, entonces (si A y B llaman super) obtengo el inicio de B siendo llamado varias veces.
Adam Parkin
3
@AdamParkin (con respecto a su pregunta como editada): si una de las clases primarias no está diseñada para usarse con super () , generalmente puede ajustarse de una manera que agregue la súper llamada. El artículo al que se hace referencia muestra un ejemplo resuelto en la sección titulada "Cómo incorporar una clase no cooperativa".
Raymond Hettinger
1
De alguna manera me las arreglé para perder esa sección cuando leí el artículo. Exactamente lo que estaba buscando. ¡Gracias!
Adam Parkin
1
Si está escribiendo python (¡ojalá 3!) Y está utilizando una herencia de cualquier tipo, pero especialmente múltiple, entonces se debe requerir la lectura de rhettinger.wordpress.com/2011/05/26/super-considered-super .
Shawn Mehan
1
Votación porque finalmente sabemos por qué no tenemos autos voladores cuando estábamos seguros de que lo tendríamos ahora.
msouth
66

La respuesta a su pregunta depende de un aspecto muy importante: ¿Están sus clases base diseñadas para herencia múltiple?

Hay 3 escenarios diferentes:

  1. Las clases base son clases independientes no relacionadas.

    Si sus clases base son entidades separadas que son capaces de funcionar de forma independiente y que no se conocen entre sí, que están no diseñados para la herencia múltiple. Ejemplo:

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

    Importante: ¡ Tenga en cuenta que Fooni las Barllamadas super().__init__()! Es por eso que su código no funcionó correctamente. Debido a la forma en que funciona la herencia de diamantes en python, las clases cuya clase base es objectno deberían llamarsuper().__init__() . Como habrás notado, hacerlo rompería la herencia múltiple porque terminas llamando a otra clase en __init__lugar de hacerlo object.__init__(). ( Descargo de responsabilidad: evitar super().__init__()en objectsubclases es mi recomendación personal y de ninguna manera es un consenso acordado en la comunidad de Python. Algunas personas prefieren usar superen cada clase, argumentando que siempre se puede escribir un adaptador si la clase no se comporta como tu esperas.)

    Esto también significa que nunca debe escribir una clase que herede objecty no tenga un __init__método. No definir un __init__método tiene el mismo efecto que llamar super().__init__(). Si su clase hereda directamente de object, asegúrese de agregar un constructor vacío así:

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

    De todos modos, en esta situación, deberá llamar a cada constructor principal manualmente. Hay dos maneras de hacer esto:

    • Sin 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

    Cada uno de estos dos métodos tiene sus propias ventajas y desventajas. Si lo usa super, su clase admitirá la inyección de dependencia . Por otro lado, es más fácil cometer errores. Por ejemplo, si cambia el orden de Fooy Bar(me gusta class FooBar(Bar, Foo)), tendría que actualizar las superllamadas para que coincidan. Sin superesto, no tiene que preocuparse por esto, y el código es mucho más legible.

  2. Una de las clases es un mixin.

    Un mixin es una clase que está diseñada para usarse con herencia múltiple. Esto significa que no tenemos que llamar a ambos constructores principales manualmente, porque el mixin llamará automáticamente al segundo constructor por nosotros. Como solo tenemos que llamar a un solo constructor esta vez, podemos hacerlo superpara evitar tener que codificar el nombre de la clase principal.

    Ejemplo:

    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.

    Los detalles importantes aquí son:

    • El mixin llama super().__init__()y pasa por cualquier argumento que reciba.
    • Los hereda de subclases de la mixin primero : class FooBar(FooMixin, Bar). Si el orden de las clases base es incorrecto, nunca se llamará al constructor del mixin.
  3. Todas las clases base están diseñadas para la herencia cooperativa.

    Las clases diseñadas para la herencia cooperativa son muy parecidas a los mixins: pasan todos los argumentos no utilizados a la siguiente clase. Como antes, solo tenemos que llamar super().__init__()y todos los constructores principales serán llamados en cadena.

    Ejemplo:

    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

    En este caso, el orden de las clases primarias no importa. También podríamos heredar desde el CoopBarprincipio, y el código seguiría funcionando igual. Pero eso solo es cierto porque todos los argumentos se pasan como argumentos de palabras clave. El uso de argumentos posicionales facilitaría que el orden de los argumentos fuera incorrecto, por lo que es habitual que las clases cooperativas acepten solo argumentos de palabras clave.

    Esto también es una excepción a la regla que mencioné anteriormente: Ambos CoopFooy CoopBarheredar de object, pero todavía llaman super().__init__(). Si no lo hicieran, no habría herencia cooperativa.

En pocas palabras: la implementación correcta depende de las clases de las que está heredando.

El constructor es parte de la interfaz pública de una clase. Si la clase está diseñada como un mixin o para una herencia cooperativa, eso debe documentarse. Si los documentos no mencionan nada por el estilo, es seguro asumir que la clase no está diseñada para la herencia cooperativa múltiple.

Aran-Fey
fuente
2
Tu segundo punto me dejó alucinado. Solo vi a Mixins a la derecha de la superclase real y pensé que eran bastante flojos y peligrosos, ya que no puedes comprobar si la clase en la que te estás mezclando tiene los atributos que esperas que tenga. Nunca pensé en poner un general super().__init__(*args, **kwargs)en el mixin y escribirlo primero. Tiene mucho sentido.
Minix
10

Cualquiera de los enfoques ("estilo nuevo" o "estilo antiguo") funcionará si tiene control sobre el código fuente de AyB . De lo contrario, podría ser necesario el uso de una clase de adaptador.

Código fuente accesible: uso correcto del "nuevo estilo"

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

Aquí, el orden de resolución de método (MRO) dicta lo siguiente:

  • C(A, B)dicta Aprimero, luego B. MRO es C -> A -> B -> object.
  • super(A, self).__init__()continúa a lo largo de la cadena de MRO iniciado en C.__init__a B.__init__.
  • super(B, self).__init__()continúa a lo largo de la cadena de MRO iniciado en C.__init__a object.__init__.

Se podría decir que este caso está diseñado para herencia múltiple .

Código fuente accesible: uso correcto del "estilo antiguo"

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

Aquí, MRO no importa, ya que A.__init__y B.__init__son llamados de manera explícita. class C(B, A):funcionaría igual de bien.

Aunque este caso no está "diseñado" para la herencia múltiple en el nuevo estilo que el anterior, todavía es posible la herencia múltiple.


Ahora, ¿qué pasa si Ay Bson de una biblioteca de terceros, es decir, usted no tiene control sobre el código fuente de AyB ? La respuesta corta: debe diseñar una clase de adaptador que implemente las superllamadas necesarias , luego use una clase vacía para definir el MRO (consulte el artículo de Raymond Hettinger sobresuper , especialmente la sección "Cómo incorporar una clase no cooperativa").

Padres de terceros: Ano implementa super; Bhace

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 clase se Adapterimplementa superpara que Cpueda definir el MRO, que entra en juego cuando super(Adapter, self).__init__()se ejecuta.

¿Y qué pasa si es al revés?

Padres de terceros: Aimplementos super; Bno

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

El mismo patrón aquí, excepto que el orden de ejecución está activado Adapter.__init__; superllame primero, luego llamada explícita. Tenga en cuenta que cada caso con padres de terceros requiere una clase de adaptador única.

Por lo tanto, parece que a menos que sepa / controle los inicios de las clases que heredo de ( Ay B) no puedo tomar una decisión segura para la clase que estoy escribiendo ( C).

Aunque puede manejar los casos en los que no controla el código fuente Ay Bmediante el uso de una clase de adaptador, es cierto que debe saber cómo se implementan los init de las clases primarias super(si es que lo hace) para hacerlo.

Nathaniel Jones
fuente
4

Como dijo Raymond en su respuesta, una llamada directa A.__init__y B.__init__funciona bien, y su código sería legible.

Sin embargo, no utiliza el enlace de herencia entre Cy esas clases. Explotar ese enlace le brinda más consistencia y hace que las refactorizaciones eventuales sean más fáciles y menos propensas a errores. Un ejemplo de cómo hacer eso:

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
fuente
1
La mejor respuesta en mi humilde opinión. Encontré esto particularmente útil ya que es más a prueba de futuro
Stephen Ellwood
3

Este artículo ayuda a explicar la herencia múltiple cooperativa:

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

Menciona el método útil mro()que le muestra el orden de resolución del método. En el segundo ejemplo, cuando usted llama superen Ala superllamada continúa en MRO. La siguiente clase en el orden es B, esta es la razón por Bla cual se llama a init por primera vez.

Aquí hay un artículo más técnico del sitio oficial de Python:

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

Kelvin
fuente
2

Si está multiplicando las clases de subclasificación de bibliotecas de terceros, entonces no, no existe un enfoque ciego para llamar a los __init__métodos de clase base (o cualquier otro método) que realmente funcione independientemente de cómo se programen las clases base.

superhace posible escribir clases diseñadas para implementar métodos cooperativamente como parte de complejos árboles de herencia múltiple que no necesitan ser conocidos por el autor de la clase. Pero no hay forma de usarlo para heredar correctamente de clases arbitrarias que pueden o no usar super.

Esencialmente, si una clase está diseñada para subclasificarse usando supero con llamadas directas a la clase base es una propiedad que forma parte de la "interfaz pública" de la clase, y debe documentarse como tal. Si está utilizando bibliotecas de terceros de la manera que el autor de la biblioteca esperaba y la biblioteca tiene documentación razonable, normalmente le dirá qué debe hacer para subclasificar cosas particulares. De lo contrario, tendrá que mirar el código fuente de las clases que está subclasificando y ver cuál es su convención de invocación de clase base. Si está combinando varias clases de una o más bibliotecas de terceros de una manera que los autores de la biblioteca no esperaban, es posible que no sea posible invocar consistentemente métodos de superclase; si la clase A es parte de una jerarquía que usa supery la clase B es parte de una jerarquía que no usa super, entonces ninguna de las opciones está garantizada para funcionar. Tendrá que descubrir una estrategia que funcione para cada caso en particular.

Ben
fuente
@RaymondHettinger Bueno, ya escribiste y vinculaste a un artículo con algunas reflexiones al respecto en tu respuesta, así que no creo que tenga mucho que añadir a eso. :) Sin embargo, no creo que sea posible adaptar genéricamente ninguna clase que no sea súper usuaria a una súper-jerarquía; tiene que encontrar una solución adaptada a las clases particulares involucradas.
Ben