herencia en Python ejercicios resueltos super override chuletario

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.


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: nombre y edad
  • Método no abstracto describir() que usa sonido() y moverse()
  • __str__ y __repr__

Requisitos de cada subclase:

  • Perro(nombre, edad, raza) — sonido: ladra, movimiento: corre
  • Gato(nombre, edad, indoor) — sonido: maúlla, movimiento: camina sigilosamente
  • Pajaro(nombre, edad, puede_volar) — sonido: canta, movimiento: vuela o camina según puede_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 de movimientos
  • Métodos abstractos: tipo(), interes_anual(), puede_retirar(cantidad)
  • Métodos no abstractos: depositar(cantidad), retirar(cantidad), aplicar_interes(), extracto()
  • retirar llama a puede_retirar antes 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á en Cuenta y llama a self.puede_retirar(cantidad) — método abstracto que cada subclase implementa
  • aplicar_interes() está en Cuenta y usa self.interes_anual() — abstracto
  • El error de super().__init__() es crítico aquí — si lo olvidas en CuentaAhorro, el atributo __saldo de Cuenta no existe y todos los métodos heredados fallan
  • Para la penalización de CuentaInversion descuenta 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}'

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *