La clase Libro de la lección anterior funciona, pero Papyrus no vende solo libros. Ana quiere incorporar revistas literarias (con número de ejemplar) y Pau insiste en probar con libros digitales, que no ocupan estantería ni tienen stock físico. Copiar la clase Libro tres veces y retocarla sería repetir código — y ya sabes desde el módulo 3 que el código repetido es código que se desincroniza. La herencia resuelve el problema: una clase base (Producto) concentra lo común, y cada clase derivada añade o ajusta solo lo que la hace distinta.

Contenido

  1. El problema: tres artículos, un 80 % de código común
  2. Clase base y clase derivada
  3. super().__init__(): reutilizar la inicialización del padre
  4. Extender vs sobrescribir métodos
  5. La jerarquía completa de Papyrus
  6. isinstance() e issubclass()
  7. Cuándo NO heredar: la composición como alternativa
  8. El MRO: en qué orden busca Python
  9. Errores comunes y consejos
  10. Ejercicios con soluciones

El problema: tres artículos, un 80 % de código común

Libros, revistas y libros digitales comparten casi todo: título, precio, la fórmula de precio de socio, la comprobación de stock. Difieren en detalles: la revista tiene número; el libro digital tiene formato y siempre está disponible. La herencia expresa esa relación "es un": una Revista es un Producto, un LibroDigital es un Libro.

Clase base y clase derivada

Primero, la clase base. Generalizamos precio_socio() de 05-01 en un método más flexible, precio_final(socio=False), que reproduce exactamente la firma de la vieja función de papyrus_utils.py — otra pieza del módulo 3 que se muda a vivir con sus datos:

class Producto:
    """Cualquier artículo a la venta en Papyrus."""

    IVA_LIBROS = 0.04
    DESCUENTO_SOCIO = 0.05

    def __init__(self, titulo, precio, stock=0):
        self.titulo = titulo
        self.precio = precio
        self.stock = stock

    def precio_final(self, socio=False):
        """Precio con IVA, y con descuento si el cliente es socio."""
        descuento = Producto.DESCUENTO_SOCIO if socio else 0
        bruto = self.precio * (1 - descuento) * (1 + Producto.IVA_LIBROS)
        return round(bruto, 2)

    def hay_stock(self):
        return self.stock > 0

    def descripcion(self):
        return f"{self.titulo} — {self.precio_final():.2f} €"

Para derivar una clase, se escribe la base entre paréntesis:

class Revista(Producto):
    """Una revista literaria: como un Producto, pero con número."""

    def __init__(self, titulo, numero, precio, stock=0):
        super().__init__(titulo, precio, stock)   # delega lo común en Producto
        self.numero = numero                      # y añade lo suyo

Revista hereda todo lo que no redefine: precio_final(), hay_stock(), los atributos de clase. Ya funciona:

quimera = Revista("Quimera", 482, 6.50, 10)
print(quimera.precio_final(socio=True))   # 6.42  → la fórmula heredada de Producto
print(quimera.numero)                     # 482   → el atributo propio
print(quimera.hay_stock())                # True  → método heredado sin tocar

super().__init__(): reutilizar la inicialización del padre

super() devuelve un acceso a la clase base, y super().__init__(...) ejecuta su inicializador. Sin esa llamada, titulo, precio y stock nunca se crearían en el objeto Revista:

class RevistaMal(Producto):
    def __init__(self, titulo, numero, precio):
        self.numero = numero   # ¡olvidó llamar a super().__init__!

r = RevistaMal("Quimera", 482, 6.50)
r.precio_final()   # AttributeError: 'RevistaMal' object has no attribute 'precio'

La regla práctica: si la derivada define su propio __init__, su primera línea casi siempre es super().__init__(...) con los argumentos que la base necesita. Si la derivada no define __init__, hereda el de la base automáticamente y no hay nada que hacer.

Extender vs sobrescribir métodos

Una clase derivada puede hacer tres cosas con cada método heredado:

Estrategia Qué hace Ejemplo en Papyrus
Heredar tal cual No lo redefine; usa el del padre Revista hereda precio_final()
Sobrescribir (override) Lo redefine por completo LibroDigital redefine hay_stock()
Extender Lo redefine, pero llama al del padre con super() y añade algo Revista.descripcion() reutiliza y decora

Sobrescribir — reemplazo total:

class LibroDigital(Libro):
    def hay_stock(self):
        return True   # un archivo digital nunca se agota

Extender — el padre hace su parte y el hijo la completa:

class Revista(Producto):
    def __init__(self, titulo, numero, precio, stock=0):
        super().__init__(titulo, precio, stock)
        self.numero = numero

    def descripcion(self):
        base = super().descripcion()          # "Quimera — 6.76 €"
        return f"[Revista n.º {self.numero}] {base}"

La jerarquía completa de Papyrus

Reunimos las piezas. Libro pasa a heredar de Producto (y aprovecha para ganar el atributo autor), y LibroDigital hereda de Libro, porque un libro digital es un libro:

class Libro(Producto):
    """Un libro en papel del catálogo."""

    def __init__(self, titulo, autor, precio, stock=0):
        super().__init__(titulo, precio, stock)
        self.autor = autor

    def descripcion(self):
        return f"[Libro] {self.titulo}, de {self.autor} — {self.precio_final():.2f} €"


class Revista(Producto):
    """Una revista literaria numerada."""

    def __init__(self, titulo, numero, precio, stock=0):
        super().__init__(titulo, precio, stock)
        self.numero = numero

    def descripcion(self):
        return f"[Revista n.º {self.numero}] {self.titulo} — {self.precio_final():.2f} €"


class LibroDigital(Libro):
    """Un libro sin ejemplares físicos: se entrega por descarga."""

    def __init__(self, titulo, autor, precio, formato="EPUB"):
        super().__init__(titulo, autor, precio)   # stock queda en su valor por defecto, 0
        self.formato = formato

    def hay_stock(self):
        return True   # sobrescrito: siempre disponible

    def descripcion(self):
        return f"[Digital {self.formato}] {self.titulo}, de {self.autor} — {self.precio_final():.2f} €"
classDiagram
    class Producto {
        +IVA_LIBROS = 0.04
        +DESCUENTO_SOCIO = 0.05
        +titulo
        +precio
        +stock
        +precio_final(socio)
        +hay_stock()
        +descripcion()
    }
    class Libro {
        +autor
        +descripcion()
    }
    class Revista {
        +numero
        +descripcion()
    }
    class LibroDigital {
        +formato
        +hay_stock()
        +descripcion()
    }
    Producto <|-- Libro
    Producto <|-- Revista
    Libro <|-- LibroDigital

En acción, con el catálogo canónico enriquecido:

articulos = [
    Libro("La Odisea", "Homero", 12.50, 4),
    Libro("Fausto", "Goethe", 21.00, 0),
    Revista("Quimera", 482, 6.50, 10),
    LibroDigital("Hamlet", "Shakespeare", 4.95, "EPUB"),
]

for articulo in articulos:
    estado = "disponible" if articulo.hay_stock() else "AGOTADO"
    print(f"{articulo.descripcion():<55} [{estado}]")
[Libro] La Odisea, de Homero — 13.00 €                  [disponible]
[Libro] Fausto, de Goethe — 21.84 €                     [AGOTADO]
[Revista n.º 482] Quimera — 6.76 €                      [disponible]
[Digital EPUB] Hamlet, de Shakespeare — 5.15 €          [disponible]

Fíjate en el detalle clave: el bucle trata a los cuatro por igual, y cada objeto responde a descripcion() y hay_stock() a su manera. Ese fenómeno tiene nombre — polimorfismo — y es el tema completo de la próxima lección.

isinstance() e issubclass()

  • isinstance(objeto, Clase) pregunta si un objeto es instancia de una clase o de cualquiera de sus derivadas.
  • issubclass(ClaseA, ClaseB) pregunta si una clase desciende de otra.
hamlet_epub = LibroDigital("Hamlet", "Shakespeare", 4.95)

isinstance(hamlet_epub, LibroDigital)   # True
isinstance(hamlet_epub, Libro)          # True  — un LibroDigital ES un Libro
isinstance(hamlet_epub, Producto)       # True  — y también un Producto
isinstance(hamlet_epub, Revista)        # False

issubclass(LibroDigital, Producto)      # True
issubclass(Revista, Libro)              # False — son hermanas, no madre e hija

Prefiere siempre isinstance(x, Producto) a type(x) is Producto: la primera respeta la herencia; la segunda solo acepta el tipo exacto. Aun así, úsalo con moderación: en 05-03 verás que encadenar isinstance para decidir comportamiento es un olor a código que el polimorfismo elimina.

Cuándo NO heredar: la composición como alternativa

La herencia modela "es un". Si la relación real es "tiene un", la herramienta correcta es la composición: guardar un objeto dentro de otro como atributo.

class Estanteria:
    """Una estantería TIENE productos; no ES un producto."""

    def __init__(self, pasillo):
        self.pasillo = pasillo
        self.productos = []   # composición: objetos Producto dentro

    def colocar(self, producto):
        self.productos.append(producto)

Señales de que heredar es mala idea:

  • Heredas solo para "aprovechar un par de métodos", pero la frase "X es un Y" suena forzada (¿una Estanteria es un Producto? No).
  • La derivada tiene que anular o vaciar la mitad de lo heredado.
  • Solo quieres reutilizar código: para eso ya tienes funciones, módulos (03-04) y composición.

Regla breve que usaremos el resto del curso: herencia para "es un", composición para "tiene un".

El MRO: en qué orden busca Python

Cuando llamas hamlet_epub.precio_final(), Python busca el método siguiendo el MRO (Method Resolution Order, orden de resolución de métodos): primero en LibroDigital, luego en Libro, luego en Producto y finalmente en object, la clase base implícita de todas las clases de Python. Se detiene en el primer sitio donde lo encuentra — por eso hay_stock() responde la versión de LibroDigital y precio_final() la de Producto.

print(LibroDigital.__mro__)
# (<class 'LibroDigital'>, <class 'Libro'>, <class 'Producto'>, <class 'object'>)

Con herencia simple (una sola base por clase, como toda la jerarquía de Papyrus) el MRO es simplemente la cadena de padres. Python también permite herencia múltipleclass C(A, B) — donde el MRO se vuelve más sutil; te basta con saber que existe y que en este curso no la necesitaremos.

Errores Comunes y Consejos

  • Olvidar super().__init__(...) en el __init__ de la derivada: los atributos del padre no se crean y los métodos heredados fallan con AttributeError al usarlos. Si defines __init__, delega primero.
  • Cambiar el orden de los argumentos al delegar. super().__init__(precio, titulo, stock) no da error inmediato: simplemente guarda el precio como título. Usa argumentos con nombre si dudas: super().__init__(titulo=titulo, precio=precio, stock=stock).
  • Heredar por conveniencia, no por semántica. Si "X es un Y" no se sostiene en voz alta, usa composición. Las jerarquías forzadas se pagan caras al crecer.
  • Sobrescribir un método y romper su contrato. Si hay_stock() devuelve bool en Producto, la versión de LibroDigital también debe devolver bool. Quien usa un Producto cualquiera confía en ese comportamiento (lo veremos formalizado como polimorfismo en 05-03).
  • Comparar tipos con type(x) is Libro cuando quieres incluir derivadas: un LibroDigital quedaría fuera. isinstance() es casi siempre lo que buscas.
  • Consejo: mantén las jerarquías poco profundas (2-3 niveles como la de Papyrus). Si necesitas un cuarto nivel, párate y pregúntate si la composición no lo resolvería mejor.

Ejercicios

Ejercicio 1: la clase Mapa

Ana empieza a vender mapas ilustrados de ciudades literarias. Crea Mapa(Producto) con un atributo extra ciudad y una descripcion() que devuelva "[Mapa] Dublín de Joyce (Dublín) — 8.32 €" (usa precio_final()). Crea uno con título "Dublín de Joyce", ciudad "Dublín", precio 8.00 y stock 3, e imprime su descripción.

Ejercicio 2: extender, no sobrescribir

Añade a LibroDigital un método descripcion() que reutilice la descripción de Libro mediante super() y le añada el sufijo " (descarga inmediata)", en lugar de construir todo el texto desde cero como hicimos arriba. Comprueba el resultado con el Hamlet digital.

Ejercicio 3: auditoría de tipos

Dada la lista articulos de la lección, escribe un fragmento que cuente cuántos artículos son Libro (incluyendo digitales) y cuántos son exactamente libros en papel (es decir, Libro pero no LibroDigital). Usa isinstance().

Soluciones

Solución 1:

class Mapa(Producto):
    """Mapa ilustrado de una ciudad literaria."""

    def __init__(self, titulo, ciudad, precio, stock=0):
        super().__init__(titulo, precio, stock)
        self.ciudad = ciudad

    def descripcion(self):
        return f"[Mapa] {self.titulo} ({self.ciudad}) — {self.precio_final():.2f} €"

dublin = Mapa("Dublín de Joyce", "Dublín", 8.00, 3)
print(dublin.descripcion())   # [Mapa] Dublín de Joyce (Dublín) — 8.32 €

Mapa solo escribe lo que le es propio (ciudad y su formato de descripción); precio, stock y la fórmula del IVA llegan gratis desde Producto.

Solución 2:

class LibroDigital(Libro):
    def __init__(self, titulo, autor, precio, formato="EPUB"):
        super().__init__(titulo, autor, precio)
        self.formato = formato

    def hay_stock(self):
        return True

    def descripcion(self):
        return super().descripcion() + " (descarga inmediata)"

hamlet_epub = LibroDigital("Hamlet", "Shakespeare", 4.95)
print(hamlet_epub.descripcion())
# [Libro] Hamlet, de Shakespeare — 5.15 € (descarga inmediata)

Esta es la versión "extender": si mañana Libro.descripcion() cambia de formato, LibroDigital se actualiza solo. Es el mismo principio DRY que motivó las funciones en el módulo 3.

Solución 3:

libros = [a for a in articulos if isinstance(a, Libro)]
solo_papel = [a for a in articulos if isinstance(a, Libro) and not isinstance(a, LibroDigital)]

print(f"Libros (papel + digital): {len(libros)}")    # 3
print(f"Solo libros en papel:     {len(solo_papel)}") # 2

isinstance(a, Libro) acepta también los LibroDigital porque heredan de Libro; para excluirlos hay que pedirlo explícitamente. Las comprensiones de listas de 02-04 siguen siendo la forma más limpia de filtrar.

Conclusión

La herencia ha convertido la clase suelta de 05-01 en una familia: Producto concentra título, precio, stock y la fórmula canónica de precio_final(); Libro añade autor; Revista, su número; y LibroDigital hereda de Libro, aporta el formato y sobrescribe hay_stock() porque lo digital no se agota. Has aprendido a delegar la inicialización con super().__init__(), a distinguir heredar, extender y sobrescribir, a preguntar por tipos con isinstance()/issubclass(), a reconocer cuándo la composición ("tiene un") es mejor que la herencia ("es un"), y a leer el MRO que gobierna la búsqueda de métodos. Pero queda pendiente la observación más interesante de la lección: aquel bucle que recorría libros, revistas y digitales llamando a descripcion() sin preguntar jamás qué era cada cosa. Ese poder — un mismo mensaje, muchas respuestas — se llama polimorfismo, y es lo que hace que las jerarquías merezcan la pena. Es el tema de la próxima lección.

Curso de Programación en Python

Módulo 1: Introducción a Python

Módulo 2: Estructuras de Control

Módulo 3: Funciones y Módulos

Módulo 4: Estructuras de Datos

Módulo 5: Programación Orientada a Objetos

Módulo 6: Manejo de Archivos

Módulo 7: Manejo de Errores y Excepciones

Módulo 8: Temas Avanzados

Módulo 9: Pruebas y Depuración

Módulo 10: Desarrollo Web con Python

Módulo 11: Ciencia de Datos con Python

Módulo 12: Proyecto Final

© Copyright 2026. Todos los derechos reservados