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
- El problema: tres artículos, un 80 % de código común
- Clase base y clase derivada
super().__init__(): reutilizar la inicialización del padre- Extender vs sobrescribir métodos
- La jerarquía completa de Papyrus
isinstance()eissubclass()- Cuándo NO heredar: la composición como alternativa
- El MRO: en qué orden busca Python
- Errores comunes y consejos
- 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 suyoRevista 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 tocarsuper().__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:
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 hijaPrefiere 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
Estanteriaes unProducto? 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últiple — class 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 conAttributeErroral 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()devuelveboolenProducto, la versión deLibroDigitaltambién debe devolverbool. Quien usa unProductocualquiera confía en ese comportamiento (lo veremos formalizado como polimorfismo en 05-03). - Comparar tipos con
type(x) is Librocuando quieres incluir derivadas: unLibroDigitalquedarí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)}") # 2isinstance(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
- Introducción a Python
- Configuración del Entorno de Desarrollo
- Sintaxis de Python y Tipos de Datos Básicos
- Variables y Constantes
- Entrada y Salida Básica
- Entornos Virtuales y Gestión de Paquetes
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for y while
- Herramientas de Control de Flujo
- Comprensiones de Listas
Módulo 3: Funciones y Módulos
- Definición de Funciones
- Argumentos de Función
- Funciones Lambda
- Módulos y Paquetes
- Visión General de la Biblioteca Estándar
Módulo 4: Estructuras de Datos
Módulo 5: Programación Orientada a Objetos
Módulo 6: Manejo de Archivos
- Lectura y Escritura de Archivos
- Trabajo con Archivos CSV
- Manejo de Datos JSON
- Operaciones con Archivos y Directorios
Módulo 7: Manejo de Errores y Excepciones
- Introducción a las Excepciones
- Manejo de Excepciones
- Lanzamiento de Excepciones
- Excepciones Personalizadas
- Buenas Prácticas y Registro de Errores con logging
Módulo 8: Temas Avanzados
- Anotaciones de Tipos (type hints)
- Decoradores
- Generadores
- Administradores de Contexto
- Concurrencia: Hilos y Procesos
- Asyncio para Programación Asíncrona
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con unittest
- Pruebas con pytest
- Desarrollo Guiado por Pruebas
- Técnicas de Depuración
- Uso de pdb para Depuración
Módulo 10: Desarrollo Web con Python
- Introducción al Desarrollo Web
- Fundamentos del Framework Flask
- Construcción de APIs REST con Flask
- Introducción a Django
- Construcción de Aplicaciones Web con Django
Módulo 11: Ciencia de Datos con Python
- Introducción a la Ciencia de Datos
- NumPy para Computación Numérica
- Pandas para Manipulación de Datos
- Matplotlib para Visualización de Datos
- Introducción al Aprendizaje Automático con scikit-learn
