¿Qué hace 'super' en Python? - diferencia entre super().__init__() y la superclase explícita __init__()
Cuál es la diferencia entre:
class Child(SomeBaseClass):
def __init__(self):
super(Child, self).__init__()
y:
class Child(SomeBaseClass):
def __init__(self):
SomeBaseClass.__init__(self)
He visto super
que se usa bastante en clases con una sola herencia. Puedo ver por qué lo usarías en herencia múltiple, pero no tengo claro cuáles son las ventajas de usarlo en este tipo de situación.
Esta pregunta trata sobre detalles técnicos de implementación y la distinción entre diferentes formas de acceder al __init__
método de la clase base. Para cerrar preguntas duplicadas en las que a OP simplemente le falta una super
llamada y pregunta por qué los atributos de la clase base no están disponibles, utilice ¿ Por qué mis instancias de subclase no contienen los atributos de la clase base (provocando un AttributeError cuando intento usarlos)? ? en cambio.
¿Cual es la diferencia?
SomeBaseClass.__init__(self)
significa llamar SomeBaseClass
's __init__
. mientras
super().__init__()
significa llamar a un límite __init__
desde la clase principal que sigue SomeBaseClass
a la clase secundaria (la que define este método) en el Orden de resolución de métodos (MRO) de la instancia.
Si la instancia es una subclase de esta clase secundaria, puede haber un padre diferente a continuación en el MRO.
Explicado simplemente
Cuando escribes una clase, quieres que otras clases puedan usarla. super()
hace que sea más fácil para otras clases usar la clase que estás escribiendo.
Como dice Bob Martin, una buena arquitectura permite posponer la toma de decisiones el mayor tiempo posible.
super()
puede permitir ese tipo de arquitectura.
Cuando otra clase crea una subclase de la clase que usted escribió, también podría estar heredando de otras clases. Y esas clases podrían tener una __init__
que venga después de esto __init__
según el orden de las clases para la resolución del método.
Sin super
usted, probablemente codificaría el padre de la clase que está escribiendo (como lo hace el ejemplo). Esto significaría que no llamaría al siguiente __init__
en el MRO y, por lo tanto, no podría reutilizar el código que contiene.
Si está escribiendo su propio código para uso personal, es posible que no le importe esta distinción. Pero si desea que otros usen su código, usarlo super
es algo que permite una mayor flexibilidad para los usuarios del código.
Pitón 2 contra 3
Esto funciona en Python 2 y 3:
super(Child, self).__init__()
Esto sólo funciona en Python 3:
super().__init__()
Funciona sin argumentos moviéndose hacia arriba en el marco de la pila y obteniendo el primer argumento del método (generalmente self
para un método de instancia o cls
para un método de clase, pero podrían tener otros nombres) y buscando la clase (por ejemplo, Child
) en las variables libres ( se busca con el nombre __class__
como variable de cierre libre en el método).
Solía preferir demostrar la forma de uso compatible con super
, pero ahora que Python 2 está en gran medida obsoleto, demostraré la forma de hacer las cosas de Python 3, es decir, llamar super
sin argumentos.
Indirección con compatibilidad hacia adelante
¿Qué te da? Para la herencia única, los ejemplos de la pregunta son prácticamente idénticos desde el punto de vista del análisis estático. Sin embargo, el uso super
le brinda una capa de direccionamiento indirecto con compatibilidad hacia adelante.
La compatibilidad con versiones posteriores es muy importante para los desarrolladores experimentados. Quiere que su código siga funcionando con cambios mínimos a medida que lo cambia. Cuando miras tu historial de revisiones, quieres ver exactamente qué cambió y cuándo.
Puede comenzar con herencia única, pero si decide agregar otra clase base, solo tiene que cambiar la línea con las bases; si las bases cambian en una clase de la que hereda (digamos que se agrega un mixin), cambiaría nada en esta clase.
En Python 2, obtener los argumentos correctos super
y los argumentos del método correcto puede ser un poco confuso, por lo que sugiero usar el método exclusivo de Python 3 para llamarlo.
Si sabe que está utilizando super
correctamente la herencia única, la depuración será menos difícil en el futuro.
Inyección de dependencia
Otras personas pueden usar su código e insertar padres en la resolución del método:
class SomeBaseClass(object):
def __init__(self):
print('SomeBaseClass.__init__(self) called')
class UnsuperChild(SomeBaseClass):
def __init__(self):
print('UnsuperChild.__init__(self) called')
SomeBaseClass.__init__(self)
class SuperChild(SomeBaseClass):
def __init__(self):
print('SuperChild.__init__(self) called')
super().__init__()
Supongamos que agrega otra clase a su objeto y desea inyectar una clase entre Foo y Bar (para pruebas o por algún otro motivo):
class InjectMe(SomeBaseClass):
def __init__(self):
print('InjectMe.__init__(self) called')
super().__init__()
class UnsuperInjector(UnsuperChild, InjectMe): pass
class SuperInjector(SuperChild, InjectMe): pass
El uso del hijo no-super no logra inyectar la dependencia porque el hijo que estás usando ha codificado el método que se llamará después del suyo:
>>> o = UnsuperInjector()
UnsuperChild.__init__(self) called
SomeBaseClass.__init__(self) called
Sin embargo, la clase con el niño que usa super
puede inyectar correctamente la dependencia:
>>> o2 = SuperInjector()
SuperChild.__init__(self) called
InjectMe.__init__(self) called
SomeBaseClass.__init__(self) called
Dirigir un comentario
¿Por qué sería útil esto?
Python linealiza un árbol de herencia complicado mediante el algoritmo de linealización C3 para crear un orden de resolución de método (MRO).
Queremos que los métodos se busquen en ese orden .
Para que un método definido en un padre encuentre el siguiente en ese orden sin super
, tendría que
- obtener el mro del tipo de instancia
- busca el tipo que define el método
- encuentra el siguiente tipo con el método
- vincular ese método y llamarlo con los argumentos esperados
No
UnsuperChild
deberían tener acceso aInjectMe
. ¿Por qué la conclusión no es "Evite siempre usarsuper
"? ¿Que me estoy perdiendo aqui?
El UnsuperChild
no tiene acceso a InjectMe
. Es el UnsuperInjector
que tiene acceso InjectMe
y aún no puede llamar al método de esa clase desde el método del que hereda UnsuperChild
.
Ambas clases Child tienen la intención de llamar a un método con el mismo nombre que aparece a continuación en el MRO, que podría ser otra clase que no conocía cuando se creó.
El que no super
codifica el método de su padre; por lo tanto, ha restringido el comportamiento de su método y las subclases no pueden inyectar funcionalidad en la cadena de llamadas.
El que tiene super
tiene mayor flexibilidad. La cadena de llamadas de los métodos se puede interceptar y se puede inyectar funcionalidad.
Es posible que no necesite esa funcionalidad, pero los subclasificadores de su código sí pueden.
Conclusión
Úselo siempre super
para hacer referencia a la clase principal en lugar de codificarla.
Lo que pretende es hacer referencia a la clase principal que es la siguiente en la línea, no específicamente a aquella de la que hereda el hijo.
No usarlo super
puede imponer restricciones innecesarias a los usuarios de su código.
Los beneficios de super()
la herencia única son mínimos; principalmente, no es necesario codificar el nombre de la clase base en cada método que utiliza sus métodos principales.
Sin embargo, es casi imposible utilizar la herencia múltiple sin super()
. Esto incluye modismos comunes como mixins, interfaces, clases abstractas, etc. Esto se extiende al código que luego extiende el suyo. Si alguien más tarde quisiera escribir una clase extendida Child
y un mixin, su código no funcionaría correctamente.
Jugué un poco con super()
y reconocí que podemos cambiar el orden de las llamadas.
Por ejemplo, tenemos la siguiente estructura jerárquica:
A
/ \
B C
\ /
D
En este caso MRO de D será (sólo para Python 3):
In [26]: D.__mro__
Out[26]: (__main__.D, __main__.B, __main__.C, __main__.A, object)
Creemos una clase donde se realicen super()
llamadas después de la ejecución del método.
In [23]: class A(object): # or with Python 3 can define class A:
...: def __init__(self):
...: print("I'm from A")
...:
...: class B(A):
...: def __init__(self):
...: print("I'm from B")
...: super().__init__()
...:
...: class C(A):
...: def __init__(self):
...: print("I'm from C")
...: super().__init__()
...:
...: class D(B, C):
...: def __init__(self):
...: print("I'm from D")
...: super().__init__()
...: d = D()
...:
I'm from D
I'm from B
I'm from C
I'm from A
A
/ ⇖
B ⇒ C
⇖ /
D
Entonces podemos ver que el orden de resolución es el mismo que en MRO. Pero cuando llamamos super()
al comienzo del método:
In [21]: class A(object): # or class A:
...: def __init__(self):
...: print("I'm from A")
...:
...: class B(A):
...: def __init__(self):
...: super().__init__() # or super(B, self).__init_()
...: print("I'm from B")
...:
...: class C(A):
...: def __init__(self):
...: super().__init__()
...: print("I'm from C")
...:
...: class D(B, C):
...: def __init__(self):
...: super().__init__()
...: print("I'm from D")
...: d = D()
...:
I'm from A
I'm from C
I'm from B
I'm from D
Tenemos un orden diferente, es un orden inverso de la tupla MRO.
A
/ ⇘
B ⇐ C
⇘ /
D
Para lecturas adicionales, recomendaría las siguientes respuestas:
- Ejemplo de linealización C3 con super (una gran jerarquía)
- Cambios de comportamiento importantes entre las clases de estilo antiguas y nuevas
- La historia interna de las clases de nuevo estilo