Saltar al contenido principal

Introducción a la herencia múltiple y super()

Guía introductoria de un Pythonista a la herencia múltiple, la función super() y cómo sortear el problema del diamante.
Actualizado 16 ene 2025  · 8 min de lectura

Visión rápida de la herencia

A medida que hagas crecer tus proyectos y paquetes Python, inevitablemente querrás utilizar clases y aplicar el principio DRY (don't-repeat-yourself) mientras lo haces. La herencia de clases es una forma fantástica de crear una clase basada en otra clase para permanecer DRY. En este post se tratarán conceptos más avanzados de la herencia, y no se profundizará en la herencia básica. Haremos una introducción rápida, pero hay introducciones mucho mejores y detalladas por ahí. Aquí tienes algunos recursos para empezar: Curso de Programación Orientada a Objetos en Python & Programación Orientada a Objetos en Python (POO): Tutorial.

¿Qué es la herencia de clases? De forma similar a la genética, una clase hija puede "heredar" atributos y métodos de una clase padre. Veamos un ejemplo de código. En el siguiente bloque de código demostraremos la herencia con una clase Child que hereda de una clase Parent.

Entrada

class Parent:
    def __init__(self):
        self.parent_attribute = 'I am a parent'

    def parent_method(self):
        print('Back in my day...')


# Create a child class that inherits from Parent
class Child(Parent):
    def __init__(self):
        Parent.__init__(self)
        self.child_attribute = 'I am a child'


# Create instance of child
child = Child()

# Show attributes and methods of child class
print(child.child_attribute)
print(child.parent_attribute)
child.parent_method()

Salida

I am a child
I am a parent
Back in my day...

Vemos que la clase Child "hereda" atributos y métodos de la clase Parent. Sin ningún trabajo por nuestra parte, el Parent.parent_method forma parte de la clase Child. Para obtener los beneficios del método Parent.__init__() necesitábamos llamar explícitamente al método y pasar self. Esto se debe a que cuando añadimos un método __init__ a Child, sobrescribimos el heredado __init__.

Una vez aclarado este breve y poco exhaustivo resumen, pasemos al meollo de la cuestión.

Introducción a super

En el caso más sencillo, se puede utilizar la función super para sustituir la llamada explícita a Parent.__init__(self). Nuestro ejemplo de introducción de la primera sección puede reescribirse con super como se ve a continuación. Ten en cuenta que el siguiente bloque de código está escrito en Python 3, las versiones anteriores utilizan una sintaxis ligeramente diferente. Además, se ha omitido la salida, ya que es idéntica al primer bloque de código.

class Parent:
    def __init__(self):
        self.parent_attribute = 'I am a parent'

    def parent_method(self):
        print('Back in my day...')


# Create a child class that inherits from Parent
class Child(Parent):
    def __init__(self):
        super().__init__()
        self.child_attribute = 'I am a parent'


# Create instance of child
child = Child()

# Show attributes and methods of child class
print(child.child_attribute)
print(child.parent_attribute)
child.parent_method()

Para ser sincero, super en este caso nos da poca ventaja, si es que nos da alguna. Dependiendo del nombre de nuestra clase padre podríamos ahorrarnos algunas pulsaciones, y no tenemos que pasar self a la llamada a __init__. A continuación se exponen algunos pros y contras del uso de super en casos de herencia única.

Contras

Se puede argumentar que utilizar super aquí hace que el código sea menos explícito. Hacer el código menos explícito viola El Zen de Python, que afirma: "Lo explícito es mejor que lo implícito".

Pros

Se puede argumentar a favor de la mantenibilidad de super incluso en la herencia simple. Si por cualquier motivo tu clase hija cambia su patrón de herencia (es decir, cambia la clase padre o se pasa a la herencia múltiple), no es necesario encontrar y reemplazar todas las referencias persistentes a ParentClass.method_name(); el uso de super permitirá que todos los cambios fluyan con el cambio en la declaración class.

Domina tus habilidades de datos con DataCamp

Más de 10 millones de personas aprenden Python, R, SQL y otras habilidades tecnológicas con nuestros cursos prácticos elaborados por expertos del sector.

Empieza a aprender
learner-on-couch@2x.jpg

super y herencia múltiple

Antes de entrar en la herencia múltiple y super... Atención, esto puede volverse bastante raro y complicado.

En primer lugar, ¿qué es la herencia múltiple? Hasta ahora, el código de ejemplo ha abarcado una única clase hija que hereda de una única clase padre. En la herencia múltiple, hay más de una clase padre. Una clase hija puede heredar de 2, 3, 10, etc. clases padre.

Aquí es donde las ventajas de super se hacen más evidentes. Además de ahorrarte las pulsaciones de teclas de referenciar los distintos nombres de las clases padre, utilizar super con múltiples patrones de herencia tiene ventajas matizadas. En resumen, si vas a utilizar la herencia múltiple, utiliza super.

Herencia múltiple sin super

Veamos un ejemplo de herencia múltiple que evita modificar cualquier método padre y, a su vez, evita super.

Entrada

class B:
    def b(self):
        print('b')


class C:
    def c(self):
        print('c')


class D(B, C):
    def d(self):
        print('d')


d = D()
d.b()
d.c()
d.d()

Salida

b
c
d

Orden de resolución múltiple

Este resultado no es demasiado sorprendente, dado el concepto de herencia múltiple. D heredó los métodos x y z de sus clases padre, y todo va bien en el mundo... por ahora.

¿Y si tanto B como C tuvieran un método con el mismo nombre? Aquí es donde entra en juego un concepto llamado "orden de resolución múltiple" o MRO, por sus siglas en inglés. La MRO de una clase hija es lo que decide dónde buscará Python un método determinado, y a qué método llamará cuando haya un conflicto.

Veamos un ejemplo.

Entrada

class B:
    def x(self):
        print('x: B')


class C:
    def x(self):
        print('x: C')


class D(B, C):
    pass


d = D()
d.x()
print(D.mro())

Salida

x: B
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class 'object'>]

Cuando llamamos al método heredado x, sólo vemos la salida heredada de B. Podemos ver el MRO de nuestra clase D llamando al método de la clase mro. De la salida de D.mro() aprendemos lo siguiente: nuestro programa intentará llamar a los métodos de D por defecto, luego recurrirá a B, después a C, y finalmente a object. Si no se encuentra en ninguno de esos lugares, obtendremos el error de que D no tiene el método que pedimos.

Cabe señalar que, por defecto, todas las clases heredan de object, y está en la cola de todas las MRO.

Herencia múltiple, supery el problema del diamante

A continuación se muestra un ejemplo de uso de super para gestionar MRO de init de forma beneficiosa. En el ejemplo, creamos una serie de clases de tratamiento de texto y combinamos su funcionalidad en otra clase con herencia múltiple. Crearemos 4 clases, y la estructura para la herencia seguirá la estructura del diagrama siguiente.

Nota: Esta estructura tiene fines ilustrativos y, salvo restricciones, habría mejores formas de aplicar esta lógica.

problema del diamante

Esto es en realidad un ejemplo del "problema del diamante" de la herencia múltiple. Su nombre se basa, por supuesto, en la forma de su diseño, y en el hecho de que es un problema bastante confuso.

A continuación se escribe el diseño utilizando super.

Entrada

class Tokenizer:
    """Tokenize text"""
    def __init__(self, text):
        print('Start Tokenizer.__init__()')
        self.tokens = text.split()
        print('End Tokenizer.__init__()')


class WordCounter(Tokenizer):
    """Count words in text"""
    def __init__(self, text):
        print('Start WordCounter.__init__()')
        super().__init__(text)
        self.word_count = len(self.tokens)
        print('End WordCounter.__init__()')


class Vocabulary(Tokenizer):
    """Find unique words in text"""
    def __init__(self, text):
        print('Start init Vocabulary.__init__()')
        super().__init__(text)
        self.vocab = set(self.tokens)
        print('End init Vocabulary.__init__()')


class TextDescriber(WordCounter, Vocabulary):
    """Describe text with multiple metrics"""
    def __init__(self, text):
        print('Start init TextDescriber.__init__()')
        super().__init__(text)
        print('End init TextDescriber.__init__()')


td = TextDescriber('row row row your boat')
print('--------')
print(td.tokens)
print(td.vocab)
print(td.word_count)

Salida

Start init TextDescriber.__init__()
Start WordCounter.__init__()
Start init Vocabulary.__init__()
Start Tokenizer.__init__()
End Tokenizer.__init__()
End init Vocabulary.__init__()
End WordCounter.__init__()
End init TextDescriber.__init__()
--------
['row', 'row', 'row', 'your', 'boat']
{'boat', 'your', 'row'}
5

En primer lugar, vemos que la clase TextDescriber ha heredado todos los atributos del árbol genealógico de clases. Gracias a la herencia múltiple podemos "combinar" la funcionalidad de más de una clase.

Hablemos ahora de las impresiones procedentes de los métodos init de la clase:

Cada __init__ fue llamado una y sólo una vez.

La clase TextDescriber hereda de 2 clases que heredan de Tokenizer. ¿Por qué no se llamó dos veces a Tokenizer.__init__?

Si sustituyéramos todas nuestras llamadas a super por el método antiguo, acabaríamos teniendo 2 llamadas a Tokenizer.__init__. Las llamadas a super "piensan" un poco más en nuestro patrón y se saltan el viaje extra a A.

Cada __init__ se inició antes de que se terminaran los demás.

Merece la pena tener en cuenta el orden de inicio y fin de cada __init__ por si intentas establecer un atributo que tiene un conflicto de nombres con otra clase padre. El atributo se sobrescribirá, y puede resultar muy confuso.

En nuestro caso, hemos evitado los conflictos de nombres con los atributos heredados, así que todo funciona como se esperaba.

Para reiterar, el problema del diamante puede complicarse rápidamente y conducir a resultados inesperados. En la mayoría de los casos de programación, es mejor evitar los diseños complicados.

Lo que hemos aprendido

  • Hemos aprendido sobre la función super y cómo puede utilizarse para sustituir a ParentName.method en la herencia simple. Ésta puede ser una práctica más fácil de mantener.
  • Hemos aprendido sobre la herencia múltiple y cómo podemos pasar la funcionalidad de varias clases padre a una única clase hijo.
  • Hemos aprendido sobre el orden de resolución múltiple y cómo decide lo que ocurre en la herencia múltiple cuando hay un conflicto de nombres entre métodos padre.
  • Aprendimos sobre el problema del diamante y vimos un ejemplo de cómo el uso de super navega por el diamante.
Temas

Más información sobre Python

Certificación disponible

curso

Introducción a Python

4 hr
6M
Domina los fundamentos del análisis de datos con Python en sólo cuatro horas. Este curso online introducirá la interfaz de Python y explorará paquetes populares.
Ver detallesRight Arrow
Comienza el curso
Ver másRight Arrow