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__
:
- (viejo estilo)
ParentClass.__init__(self)
- (estilo más nuevo)
super(DerivedClass, self).__init__()
Sin embargo, en cualquier caso, si las clases principales ( A
y 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 A
o B
es 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 B
el 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 B
nunca se llama a init. 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
).
fuente
Respuestas:
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 ambosA.__init__
yB.__init__
.Cuando se usa
super()
, las clases deben diseñarse para la herencia cooperativa múltiple donde seC
llamasuper
, que invocaA
el código que tambiénsuper
invocaB
el 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 hacersuper
.[Pregunta de respuesta como se editó más tarde]
El artículo al que se hace referencia muestra cómo manejar esta situación agregando una clase de contenedor alrededor de
A
yB
. 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.
fuente
super().__init__()
enfoque. Si llamoA.__init__()
yB.__init__()
directamente, entonces (si A y B llamansuper
) obtengo el inicio de B siendo llamado varias veces.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:
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:
Importante: ¡ Tenga en cuenta que
Foo
ni lasBar
llamadassuper().__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 esobject
no 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 hacerloobject.__init__()
. ( Descargo de responsabilidad: evitarsuper().__init__()
enobject
subclases es mi recomendación personal y de ninguna manera es un consenso acordado en la comunidad de Python. Algunas personas prefieren usarsuper
en 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
object
y no tenga un__init__
método. No definir un__init__
método tiene el mismo efecto que llamarsuper().__init__()
. Si su clase hereda directamente deobject
, asegúrese de agregar un constructor vacío así:De todos modos, en esta situación, deberá llamar a cada constructor principal manualmente. Hay dos maneras de hacer esto:
Sin
super
Con
super
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 deFoo
yBar
(me gustaclass FooBar(Bar, Foo)
), tendría que actualizar lassuper
llamadas para que coincidan. Sinsuper
esto, no tiene que preocuparse por esto, y el código es mucho más legible.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
super
para evitar tener que codificar el nombre de la clase principal.Ejemplo:
Los detalles importantes aquí son:
super().__init__()
y pasa por cualquier argumento que reciba.class FooBar(FooMixin, Bar)
. Si el orden de las clases base es incorrecto, nunca se llamará al constructor del mixin.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:
En este caso, el orden de las clases primarias no importa. También podríamos heredar desde el
CoopBar
principio, 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
CoopFoo
yCoopBar
heredar deobject
, pero todavía llamansuper().__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.
fuente
super().__init__(*args, **kwargs)
en el mixin y escribirlo primero. Tiene mucho sentido.Cualquiera de los enfoques ("estilo nuevo" o "estilo antiguo") funcionará si tiene control sobre el código fuente de
A
yB
. De lo contrario, podría ser necesario el uso de una clase de adaptador.Código fuente accesible: uso correcto del "nuevo estilo"
Aquí, el orden de resolución de método (MRO) dicta lo siguiente:
C(A, B)
dictaA
primero, luegoB
. MRO esC -> A -> B -> object
.super(A, self).__init__()
continúa a lo largo de la cadena de MRO iniciado enC.__init__
aB.__init__
.super(B, self).__init__()
continúa a lo largo de la cadena de MRO iniciado enC.__init__
aobject.__init__
.Se podría decir que este caso está diseñado para herencia múltiple .
Código fuente accesible: uso correcto del "estilo antiguo"
Aquí, MRO no importa, ya que
A.__init__
yB.__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
A
yB
son de una biblioteca de terceros, es decir, usted no tiene control sobre el código fuente deA
yB
? La respuesta corta: debe diseñar una clase de adaptador que implemente lassuper
llamadas 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:
A
no implementasuper
;B
haceLa clase se
Adapter
implementasuper
para queC
pueda definir el MRO, que entra en juego cuandosuper(Adapter, self).__init__()
se ejecuta.¿Y qué pasa si es al revés?
Padres de terceros:
A
implementossuper
;B
noEl mismo patrón aquí, excepto que el orden de ejecución está activado
Adapter.__init__
;super
llame primero, luego llamada explícita. Tenga en cuenta que cada caso con padres de terceros requiere una clase de adaptador única.Aunque puede manejar los casos en los que no controla el código fuente
A
yB
mediante el uso de una clase de adaptador, es cierto que debe saber cómo se implementan los init de las clases primariassuper
(si es que lo hace) para hacerlo.fuente
Como dijo Raymond en su respuesta, una llamada directa
A.__init__
yB.__init__
funciona bien, y su código sería legible.Sin embargo, no utiliza el enlace de herencia entre
C
y 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:fuente
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 llamasuper
enA
lasuper
llamada continúa en MRO. La siguiente clase en el orden esB
, esta es la razón porB
la 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/
fuente
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.super
hace 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 usarsuper
.Esencialmente, si una clase está diseñada para subclasificarse usando
super
o 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 usasuper
y 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.fuente