Herencia en Python — ejercicios para dominar jerarquías de clases
La herencia en Python ejercicios con solución cierran este bloque. Ya viste la teoría y practicaste con jerarquías reales. Ahora toca resolver por tu cuenta. Dos ejercicios en tres niveles de dificultad con especial atención al error más común en FP2 olvidar llamar a super().__init__() en la subclase y quedarse con atributos de la superclase sin inicializar. Corrige tu código con la ayuda de pythontutor.com.
Tabla de Contenidos
Herencia en Python ejercicios — Nivel Básico
Ejercicio 1 — Sistema de animales con comportamiento específico
Diseña una jerarquía de clases para representar animales. La clase base Animal debe ser abstracta con estos métodos abstractos: sonido() y moverse(). Las subclases son Perro, Gato y Pajaro.
Requisitos de la clase base Animal:
- Atributos privados:
nombreyedad - Método no abstracto
describir()que usasonido()ymoverse() __str__y__repr__
Requisitos de cada subclase:
Perro(nombre, edad, raza)— sonido: ladra, movimiento: correGato(nombre, edad, indoor)— sonido: maúlla, movimiento: camina sigilosamentePajaro(nombre, edad, puede_volar)— sonido: canta, movimiento: vuela o camina segúnpuede_volar
La salida debe ser:
Rex (Labrador, 3 años) Sonido: ¡Guau guau! Movimiento: corre moviendo la cola Descripción: Rex ladra y corre moviendo la cola Luna (Gato indoor, 5 años) Sonido: Miau... Movimiento: camina sigilosamente Descripción: Luna maúlla y camina sigilosamente Pío (Canario, 2 años) Sonido: ♪ Pío pío ♪ Movimiento: vuela libremente Descripción: Pío canta y vuela libremente Kiwi (Pingüino, 4 años) Sonido: ♪ Pío pío ♪ Movimiento: camina torpemente Descripción: Kiwi canta y camina torpemente
💡 Pista — el error de olvidar super().init():
# MAL — atributos de Animal sin inicializar
class Perro(Animal):
def __init__(self, nombre, edad, raza):
self.__raza = raza # Animal.__init__ nunca se llamó
# self.nombre → AttributeError al intentar usarlo
# BIEN — siempre llamar a super().__init__() primero
class Perro(Animal):
def __init__(self, nombre, edad, raza):
super().__init__(nombre, edad) # inicializa nombre y edad
self.__raza = raza # luego los propios
Sin super().__init__() los atributos de Animal no existen en el objeto, cualquier método heredado que los use lanzará AttributeError.
Herencia en Python ejercicios — Nivel Intermedio y Desafío
Ejercicio 2 — Jerarquía de cuentas bancarias
Diseña una jerarquía completa de cuentas bancarias. La clase base Cuenta debe ser abstracta.
Clase base Cuenta (abstracta):
- Atributos privados:
titular,saldo, lista demovimientos - Métodos abstractos:
tipo(),interes_anual(),puede_retirar(cantidad) - Métodos no abstractos:
depositar(cantidad),retirar(cantidad),aplicar_interes(),extracto() retirarllama apuede_retirarantes de ejecutar — cada subclase decide si se permite
Subclases:
CuentaCorriente(titular, saldo_inicial, descubierto_max):
- Sin interés anual (0%)
- Puede retirar hasta
saldo + descubierto_max— permite saldo negativo hasta el límite - Comisión de 15€ si el saldo baja de 0
CuentaAhorro(titular, saldo_inicial, interes):
- Interés anual configurable
- Solo puede retirar si el saldo resultante es >= 0
- Mínimo de 3 meses entre retiradas (usa un contador simplificado de operaciones)
CuentaInversion(titular, saldo_inicial, perfil_riesgo):
- Perfil de riesgo:
conservador(4%),moderado(7%),agresivo(12%) - Solo puede retirar si el saldo resultante es >= 1000 (mínimo de inversión)
- Penalización del 2% sobre la cantidad retirada
La salida debe ser:
=== CUENTA CORRIENTE === CuentaCorriente — Sergio | Saldo: 500.00€ Depositando 200€... Saldo: 700.00€ Retirando 900€... Saldo: -200.00€ (en descubierto) Comisión descubierto: -15.00€ | Saldo: -215.00€ === CUENTA AHORRO === CuentaAhorro — María | Saldo: 2000.00€ Aplicando interés 3.5%... Saldo: 2070.00€ Retirando 500€... Saldo: 1570.00€ Error: debes esperar más operaciones antes de retirar === CUENTA INVERSIÓN === CuentaInversion — Juan | Saldo: 5000.00€ Perfil: agresivo (12% anual) Aplicando interés... Saldo: 5600.00€ Retirando 2000€ (penalización 2%)... Saldo: 3560.00€ Error: saldo mínimo de inversión: 1000€
💡 Pistas:
retirar(cantidad)está enCuentay llama aself.puede_retirar(cantidad)— método abstracto que cada subclase implementaaplicar_interes()está enCuentay usaself.interes_anual()— abstracto- El error de
super().__init__()es crítico aquí — si lo olvidas enCuentaAhorro, el atributo__saldodeCuentano existe y todos los métodos heredados fallan - Para la penalización de
CuentaInversiondescuenta el 2% directamente del saldo después de retirar extracto()muestra todos los movimientos — guárdalos como strings en la lista
Soluciones Comentadas
Solución Ejercicio 1:
from abc import ABC, abstractmethod
class Animal(ABC):
def __init__(self, nombre, edad):
if not nombre or not isinstance(nombre, str):
raise ValueError('Nombre no válido')
if not isinstance(edad, int) or edad < 0:
raise ValueError('Edad no válida')
self.__nombre = nombre
self.__edad = edad
@property
def nombre(self):
return self.__nombre
@property
def edad(self):
return self.__edad
@abstractmethod
def sonido(self):
pass
@abstractmethod
def moverse(self):
pass
def describir(self):
verbo = self.sonido().split()[0].lower()
return f'{self.__nombre} {verbo} y {self.moverse()}'
def __str__(self):
return (f'{self.__nombre} ({self._info()}, {self.__edad} años)\n'
f' Sonido: {self.sonido()}\n'
f' Movimiento: {self.moverse()}\n'
f' Descripción: {self.describir()}')
@abstractmethod
def _info(self):
pass
def __repr__(self):
return f'{self.__class__.__name__}("{self.__nombre}", {self.__edad})'
class Perro(Animal):
def __init__(self, nombre, edad, raza):
super().__init__(nombre, edad) # SIEMPRE primero
self.__raza = raza
def sonido(self):
return '¡Guau guau!'
def moverse(self):
return 'corre moviendo la cola'
def _info(self):
return self.__raza
class Gato(Animal):
def __init__(self, nombre, edad, indoor=True):
super().__init__(nombre, edad)
self.__indoor = indoor
def sonido(self):
return 'Miau...'
def moverse(self):
return 'camina sigilosamente'
def _info(self):
return 'Gato indoor' if self.__indoor else 'Gato outdoor'
class Pajaro(Animal):
def __init__(self, nombre, edad, puede_volar, especie='Pájaro'):
super().__init__(nombre, edad)
self.__puede_volar = puede_volar
self.__especie = especie
def sonido(self):
return '♪ Pío pío ♪'
def moverse(self):
return 'vuela libremente' if self.__puede_volar else 'camina torpemente'
def _info(self):
return self.__especie
# Uso
animales = [
Perro('Rex', 3, 'Labrador'),
Gato('Luna', 5, indoor=True),
Pajaro('Pío', 2, True, 'Canario'),
Pajaro('Kiwi', 4, False, 'Pingüino')
]
for animal in animales:
print(animal)
print()
# No se puede instanciar Animal directamente
try:
a = Animal('Test', 1)
except TypeError as err:
print(f'Error esperado: {err}')
Solución Ejercicio 2:
from abc import ABC, abstractmethod
class ErrorCuenta(Exception):
pass
class Cuenta(ABC):
def __init__(self, titular, saldo_inicial):
if not titular or not isinstance(titular, str):
raise ValueError('Titular no válido')
if not isinstance(saldo_inicial, (int, float)) or saldo_inicial < 0:
raise ValueError('Saldo inicial no válido')
self.__titular = titular
self.__saldo = saldo_inicial
self.__movimientos = []
self._registrar(f'Apertura: +{saldo_inicial:.2f}€')
@property
def titular(self):
return self.__titular
@property
def saldo(self):
return self.__saldo
@abstractmethod
def tipo(self):
pass
@abstractmethod
def interes_anual(self):
pass
@abstractmethod
def puede_retirar(self, cantidad):
pass
def _registrar(self, movimiento):
self.__movimientos.append(movimiento)
def _modificar_saldo(self, cantidad):
self.__saldo += cantidad
def depositar(self, cantidad):
if not isinstance(cantidad, (int, float)) or cantidad <= 0:
raise ErrorCuenta('La cantidad debe ser positiva')
self.__saldo += cantidad
self._registrar(f'Depósito: +{cantidad:.2f}€ | Saldo: {self.__saldo:.2f}€')
def retirar(self, cantidad):
if not isinstance(cantidad, (int, float)) or cantidad <= 0:
raise ErrorCuenta('La cantidad debe ser positiva')
self.puede_retirar(cantidad) # cada subclase decide
self.__saldo -= cantidad
self._registrar(f'Retirada: -{cantidad:.2f}€ | Saldo: {self.__saldo:.2f}€')
self._post_retirada(cantidad) # hook para efectos secundarios
def _post_retirada(self, cantidad):
pass # las subclases pueden sobreescribir
def aplicar_interes(self):
interes = self.__saldo * self.interes_anual()
self.__saldo += interes
self._registrar(
f'Interés {self.interes_anual()*100:.1f}%: '
f'+{interes:.2f}€ | Saldo: {self.__saldo:.2f}€')
def extracto(self):
lineas = [f'=== Extracto {self.tipo()} — {self.__titular} ===']
for mov in self.__movimientos:
lineas.append(f' {mov}')
lineas.append(f' Saldo actual: {self.__saldo:.2f}€')
return '\n'.join(lineas)
def __str__(self):
return f'{self.tipo()} — {self.__titular} | Saldo: {self.__saldo:.2f}€'
def __repr__(self):
return f'{self.__class__.__name__}("{self.__titular}", {self.__saldo})'
class CuentaCorriente(Cuenta):
def __init__(self, titular, saldo_inicial, descubierto_max=500):
super().__init__(titular, saldo_inicial) # SIEMPRE primero
self.__descubierto_max = descubierto_max
def tipo(self):
return 'CuentaCorriente'
def interes_anual(self):
return 0.0
def puede_retirar(self, cantidad):
if cantidad > self.saldo + self.__descubierto_max:
raise ErrorCuenta(
f'Límite de descubierto: {self.__descubierto_max:.2f}€')
def _post_retirada(self, cantidad):
if self.saldo < 0:
self._modificar_saldo(-15)
self._registrar(
f'Comisión descubierto: -15.00€ | Saldo: {self.saldo:.2f}€')
class CuentaAhorro(Cuenta):
def __init__(self, titular, saldo_inicial, interes=0.02):
super().__init__(titular, saldo_inicial)
if not (0 <= interes <= 1):
raise ValueError('Interés no válido')
self.__interes = interes
self.__operaciones_desde_retirada = 0
def tipo(self):
return 'CuentaAhorro'
def interes_anual(self):
return self.__interes
def puede_retirar(self, cantidad):
if self.__operaciones_desde_retirada < 3:
raise ErrorCuenta(
'Debes esperar más operaciones antes de retirar')
if self.saldo - cantidad < 0:
raise ErrorCuenta('Saldo insuficiente')
def depositar(self, cantidad):
super().depositar(cantidad)
self.__operaciones_desde_retirada += 1
def _post_retirada(self, cantidad):
self.__operaciones_desde_retirada = 0
class CuentaInversion(Cuenta):
PERFILES = {'conservador': 0.04, 'moderado': 0.07, 'agresivo': 0.12}
SALDO_MINIMO = 1000
def __init__(self, titular, saldo_inicial, perfil_riesgo):
if perfil_riesgo not in self.PERFILES:
raise ValueError(f'Perfil no válido: {perfil_riesgo}')
if saldo_inicial < self.SALDO_MINIMO:
raise ValueError(f'Mínimo inicial: {self.SALDO_MINIMO}€')
super().__init__(titular, saldo_inicial)
self.__perfil = perfil_riesgo
def tipo(self):
return f'CuentaInversion ({self.__perfil})'
def interes_anual(self):
return self.PERFILES[self.__perfil]
def puede_retirar(self, cantidad):
penalizacion = cantidad * 0.02
if self.saldo - cantidad - penalizacion < self.SALDO_MINIMO:
raise ErrorCuenta(
f'Saldo mínimo de inversión: {self.SALDO_MINIMO}€')
def _post_retirada(self, cantidad):
penalizacion = cantidad * 0.02
self._modificar_saldo(-penalizacion)
self._registrar(
f'Penalización 2%: -{penalizacion:.2f}€ | Saldo: {self.saldo:.2f}€')
# Uso
print('=== CUENTA CORRIENTE ===')
cc = CuentaCorriente('Sergio', 500, descubierto_max=300)
cc.depositar(200)
cc.retirar(900)
print(cc.extracto())
print('\n=== CUENTA AHORRO ===')
ca = CuentaAhorro('María', 2000, interes=0.035)
ca.aplicar_interes()
ca.depositar(100)
ca.depositar(100)
ca.depositar(100)
ca.retirar(500)
try:
ca.retirar(200)
except ErrorCuenta as err:
print(f'Error: {err}')
print(ca.extracto())
print('\n=== CUENTA INVERSIÓN ===')
ci = CuentaInversion('Juan', 5000, 'agresivo')
ci.aplicar_interes()
ci.retirar(2000)
try:
ci.retirar(4000)
except ErrorCuenta as err:
print(f'Error: {err}')
print(ci.extracto())
Chuletario — Herencia y polimorfismo en Python
# ============================================
# CHULETARIO — Herencia y polimorfismo
# Sergio Learns · sergiolearns.com
# ============================================
# HERENCIA BÁSICA
class SubClase(SuperClase):
def __init__(self, param_propio, *params_super):
super().__init__(*params_super) # SIEMPRE primero
self.propio = param_propio # luego los propios
# OVERRIDE — redefinir método heredado
class SubClase(SuperClase):
def metodo(self): # sustituye al de SuperClase
return 'versión SubClase'
# OVERRIDE EXTENDIDO — añadir al heredado
class SubClase(SuperClase):
def metodo(self):
base = super().metodo() # llama al de SuperClase
return f'{base} + extra'
# CLASES ABSTRACTAS
from abc import ABC, abstractmethod
class MiAbstracta(ABC):
def __init__(self, param):
self.__param = param # puede tener __init__
@abstractmethod
def metodo_obligatorio(self): # subclases DEBEN implementar
pass
def metodo_normal(self): # subclases heredan
return self.metodo_obligatorio() # puede llamar a abstractos
# No instanciable directamente → TypeError
# Subclase sin implementar @abstractmethod → también abstracta → TypeError
# isinstance vs type
isinstance(obj, Clase) # True para Clase Y sus subclases
isinstance(obj, (A, B)) # True si es A o B o subclase de alguna
type(obj) == Clase # True solo para esa clase exacta
type(obj).__name__ # nombre de la clase como string
# JERARQUÍA EN DOS NIVELES
class A:
pass
class B(A):
def __init__(self):
super().__init__()
class C(B): # C hereda de B que hereda de A
def __init__(self):
super().__init__() # llama a B.__init__ que llama a A.__init__
# POLIMORFISMO CON LISTAS MIXTAS
objetos = [SubClase1(), SubClase2(), SubClase3()]
for obj in objetos:
obj.metodo() # cada uno ejecuta su versión
# ATRIBUTOS PRIVADOS Y HERENCIA
class SuperClase:
def __init__(self):
self.__privado = 0 # NO accesible desde subclases
self._protegido = 0 # accesible pero convención privado
class SubClase(SuperClase):
def metodo(self):
self.__privado # AttributeError
self._protegido # OK — convención
# HOOK PATTERN — método vacío que subclases pueden sobreescribir
class Base:
def operacion(self):
self._pre_operacion() # hook
# lógica principal
self._post_operacion() # hook
def _pre_operacion(self): pass # subclases sobreescriben si quieren
def _post_operacion(self): pass
# ERRORES TÍPICOS
# 1. Olvidar super().__init__() → atributos de superclase no inicializados
# 2. Instanciar clase abstracta → TypeError
# 3. Subclase sin implementar @abstractmethod → TypeError al instanciar
# 4. type() en vez de isinstance() → no respeta jerarquía
# 5. Llamar super() fuera de un método → RuntimeError
# 6. Hardcodear nombre de clase en vez de super() → frágil
# PATRÓN COMPLETO
from abc import ABC, abstractmethod
class Base(ABC):
def __init__(self, param):
self.__param = param
@property
def param(self): return self.__param
@abstractmethod
def comportamiento(self): pass # obligatorio
def describir(self): # usa el abstracto
return f'{self.__param}: {self.comportamiento()}'
def __str__(self): return self.describir()
def __repr__(self): return f'{self.__class__.__name__}("{self.__param}")'
class Concreta(Base):
def __init__(self, param, extra):
super().__init__(param) # SIEMPRE primero
self.__extra = extra
def comportamiento(self): # obligatorio — implementado
return f'comportamiento con {self.__extra}'
Python inheritance — exercises to master class hierarchies
Basic Level
Exercise 1 — Animal system with specific behaviour
Design an animal class hierarchy. Abstract base class Animal with abstract methods sound() and move(). Subclasses: Dog(name, age, breed), Cat(name, age, indoor), Bird(name, age, can_fly, species).
Non-abstract describe() in Animal that uses sound() and move(). Implement __str__ and __repr__.
💡 Hint — forgetting super().init():
# WRONG — Animal attributes never initialised
class Dog(Animal):
def __init__(self, name, age, breed):
self.__breed = breed # Animal.__init__ never called
# self.name → AttributeError when used
# RIGHT — always call super().__init__() first
class Dog(Animal):
def __init__(self, name, age, breed):
super().__init__(name, age) # initialises name and age
self.__breed = breed # then own attributes
Intermediate and Challenge Level
Exercise 2 — Bank account hierarchy
Abstract base class Account with abstract methods type(), annual_interest(), can_withdraw(amount). Non-abstract: deposit(amount), withdraw(amount), apply_interest(), statement().
Subclasses:
CurrentAccount(holder, balance, overdraft_limit) — no interest, allows negative balance up to limit, 15€ overdraft fee.
SavingsAccount(holder, balance, interest) — configurable interest, cannot go below 0, minimum 3 operations between withdrawals.
InvestmentAccount(holder, balance, risk_profile) — profiles: conservative (4%), moderate (7%), aggressive (12%), minimum balance 1000€, 2% withdrawal penalty.
💡 Hints: withdraw() in Account calls self.can_withdraw(amount), each subclass decides. The super().__init__() mistake is critical here — forgetting it means __balance doesn’t exist and all inherited methods fail. Use a _post_withdrawal() hook for side effects like fees and penalties.
(Solutions same as Spanish version above)
Cheat sheet — Python Inheritance and Polymorphism
# ============================================
# CHEAT SHEET — Inheritance and Polymorphism
# Sergio Learns · sergiolearns.com
# ============================================
# INHERITANCE
class SubClass(SuperClass):
def __init__(self, own_param, *super_params):
super().__init__(*super_params) # ALWAYS first
self.own = own_param # then own attributes
# OVERRIDE
class SubClass(SuperClass):
def method(self): # replaces SuperClass version
return 'SubClass version'
# EXTENDED OVERRIDE
class SubClass(SuperClass):
def method(self):
base = super().method() # calls SuperClass version
return f'{base} + extra'
# ABSTRACT CLASSES
from abc import ABC, abstractmethod
class MyAbstract(ABC):
def __init__(self, param):
self.__param = param
@abstractmethod
def required_method(self): pass # subclasses MUST implement
def normal_method(self): # subclasses inherit
return self.required_method() # can call abstract methods
# Cannot instantiate → TypeError
# Subclass missing @abstractmethod → still abstract → TypeError
# isinstance vs type
isinstance(obj, Class) # True for Class AND subclasses
isinstance(obj, (A, B)) # True if A or B or subclass of either
type(obj) == Class # True only for that exact class
# TWO-LEVEL HIERARCHY
class A: pass
class B(A):
def __init__(self): super().__init__()
class C(B):
def __init__(self): super().__init__() # calls B then A
# POLYMORPHISM
objects = [Sub1(), Sub2(), Sub3()]
for obj in objects:
obj.method() # each runs its own version
# PRIVATE ATTRIBUTES AND INHERITANCE
class Super:
def __init__(self):
self.__private = 0 # NOT accessible in subclasses
self._protected = 0 # accessible — convention only
# HOOK PATTERN
class Base:
def operation(self):
self._pre(): ...
self._post()
def _pre(self): pass # subclasses override if needed
def _post(self): pass
# COMMON MISTAKES
# 1. Forgetting super().__init__() → superclass attrs not initialised
# 2. Instantiating abstract class → TypeError
# 3. Subclass missing @abstractmethod → TypeError when instantiating
# 4. type() instead of isinstance() → ignores hierarchy
# 5. Hardcoding class name instead of super() → fragile
# FULL PATTERN
from abc import ABC, abstractmethod
class Base(ABC):
def __init__(self, param):
self.__param = param
@property
def param(self): return self.__param
@abstractmethod
def behaviour(self): pass
def describe(self):
return f'{self.__param}: {self.behaviour()}'
def __str__(self): return self.describe()
def __repr__(self): return f'{self.__class__.__name__}("{self.__param}")'
class Concrete(Base):
def __init__(self, param, extra):
super().__init__(param) # ALWAYS first
self.__extra = extra
def behaviour(self): # mandatory — implemented
return f'behaviour with {self.__extra}'
