clases en Python ejercicios resueltos chuletario encapsulamiento

Clases en Python — ejercicios para dominar objetos y encapsulamiento

Las clases en Python ejercicios con solución cierran este bloque. Ya viste la teoría y practicaste con programas reales. Ahora toca resolver por tu cuenta. Tres niveles con especial atención al error más común, modificar atributos privados directamente desde fuera de la clase saltándose el setter y perdiendo toda la validación. Cuentas con pythontutor.com para corregir los errores que vayas localizando.


Clases en Python ejercicios — Nivel Básico

Ejercicio 1 — Clase Estudiante con notas y estadísticas

Diseña una clase Estudiante que gestione las notas de un estudiante a lo largo del curso. La clase debe:

  • Tener atributos privados: nombre, edad y una lista de notas
  • Validar que el nombre no esté vacío, la edad esté entre 16 y 99 y las notas entre 0 y 10
  • Tener métodos: añadir_nota(nota), media(), nota_maxima(), nota_minima(), aprobado()
  • Tener @property para nombre, edad y notas (esta última devuelve copia)
  • Implementar __str__ y __repr__

La salida debe ser:

Estudiante: Sergio (22 años)
Notas: [7.5, 8.0, 6.5, 9.0]
Media: 7.75
Máxima: 9.0 | Mínima: 6.5
Estado: Aprobado ✓

repr: Estudiante("Sergio", 22)

💡 Pista — el error de modificar atributos privados directamente:

# MAL — salta la validación del setter
e = Estudiante('Sergio', 22)
e.__notas.append(-5)      # AttributeError — privado
e._Estudiante__notas.append(-5)  # funciona pero rompe la encapsulación

# BIEN — usa el método público
e.añadir_nota(7.5)    # valida que esté entre 0 y 10

El name mangling (_Clase__atributo) permite acceder técnicamente al atributo privado, pero hacerlo salta toda la validación. En el examen de FP2 esto cuenta como error de diseño.


Clases en Python ejercicios — Nivel Intermedio

Ejercicio 2 — Clase Vehículo con marca, modelo y kilometraje

Diseña una clase Vehiculo con encapsulamiento completo. La clase debe:

  • Tener atributos privados: marca, modelo, anio, kilometraje y activo
  • Validar que marca y modelo sean cadenas no vacías, el año entre 1900 y 2025 y el kilometraje no negativo
  • Tener métodos: conducir(km) que añade kilómetros (solo si está activo), dar_de_baja() que desactiva el vehículo, valor_estimado() que calcula el valor según la antigüedad
  • Tener un atributo de clase _total_vehiculos y método de clase total_vehiculos()
  • Implementar __str__ y __repr__

Fórmula del valor estimado:

  • Valor base: 15.000€
  • Se resta 1.000€ por cada año de antigüedad (mínimo 1.000€)
  • Se resta 0.01€ por km recorrido (mínimo 1.000€)

La salida debe ser:

Toyota Corolla (2018) — 45000 km — activo
Valor estimado: 8950.00€

Toyota Corolla (2018) — 95000 km — activo
Valor estimado: 8450.00€

Toyota Corolla (2018) — 95000 km — baja
Error: el vehículo está dado de baja

Total vehículos: 2

💡 Pistas:

  • El método conducir(km) debe validar que km sea positivo y que el vehículo esté activo, lanza ValueError en ambos casos
  • valor_estimado() usa __anio y __kilometraje directamente, no el setter, porque solo lee
  • El error de modificar directamente aparece si haces v.kilometraje = 999999, sin setter que valide, cualquier valor entraría
  • Para calcular la antigüedad usa 2025 - self.__anio

Clases en Python ejercicios — Desafío Final

Ejercicio 3 — Clase Pila (stack) con push, pop y peek

Implementa una clase Pila que simule una pila (estructura LIFO — Last In, First Out). La pila debe tener capacidad máxima configurable.

Una pila funciona así:

  • push(elemento) — añade un elemento encima de la pila
  • pop() — elimina y devuelve el elemento de encima
  • peek() — devuelve el elemento de encima sin eliminarlo
  • vacía() — devuelve True si la pila está vacía
  • llena() — devuelve True si la pila está llena

Define estas excepciones propias:

  • PilaVaciaError — cuando intentas hacer pop o peek en una pila vacía
  • PilaLlenaError — cuando intentas hacer push en una pila llena

La salida debe ser:

Pila vacía: True
Push: 10, 20, 30
Pila: [10, 20, 30] (tope: 30)
Peek: 30
Pop: 30
Pila: [10, 20] (tope: 20)
Tamaño: 2/3

Intentando push en pila llena...
Error: pila llena — capacidad máxima 3

Intentando pop en pila vacía...
Error: pila vacía — no hay elementos

repr: Pila(capacidad=3, elementos=[10, 20])

💡 Pistas:

  • Guarda los elementos en una lista privada __elementos
  • La capacidad máxima es un atributo privado de solo lectura, @property sin setter
  • push debe comprobar si está llena antes de añadir
  • pop y peek deben comprobar si está vacía antes de actuar
  • El error de atributo privado aquí sería acceder a pila._Pila__elementos.append(x) directamente, salta la comprobación de capacidad máxima
  • __str__ muestra los elementos y el tope, __repr__ muestra la capacidad y los elementos

Soluciones Comentadas

Solución Ejercicio 1:

class Estudiante:
    def __init__(self, nombre, edad):
        self.nombre = nombre    # usa setter
        self.edad = edad        # usa setter
        self.__notas = []

    @property
    def nombre(self):
        return self.__nombre

    @nombre.setter
    def nombre(self, valor):
        if not valor or not isinstance(valor, str):
            raise ValueError('El nombre debe ser una cadena no vacía')
        self.__nombre = valor.strip()

    @property
    def edad(self):
        return self.__edad

    @edad.setter
    def edad(self, valor):
        if not isinstance(valor, int) or not (16 <= valor <= 99):
            raise ValueError(f'Edad no válida: {valor}')
        self.__edad = valor

    @property
    def notas(self):
        return list(self.__notas)    # copia — no el original

    def añadir_nota(self, nota):
        if not isinstance(nota, (int, float)) or not (0 <= nota <= 10):
            raise ValueError(f'Nota no válida: {nota}')
        self.__notas.append(float(nota))

    def media(self):
        if not self.__notas:
            return 0.0
        return round(sum(self.__notas) / len(self.__notas), 2)

    def nota_maxima(self):
        if not self.__notas:
            raise ValueError('No hay notas')
        return max(self.__notas)

    def nota_minima(self):
        if not self.__notas:
            raise ValueError('No hay notas')
        return min(self.__notas)

    def aprobado(self):
        return self.media() >= 5.0

    def __str__(self):
        estado = 'Aprobado ✓' if self.aprobado() else 'Suspenso ✗'
        return (f'Estudiante: {self.__nombre} ({self.__edad} años)\n'
                f'Notas: {self.__notas}\n'
                f'Media: {self.media()}\n'
                f'Máxima: {self.nota_maxima()} | Mínima: {self.nota_minima()}\n'
                f'Estado: {estado}')

    def __repr__(self):
        return f'Estudiante("{self.__nombre}", {self.__edad})'

# Uso
e = Estudiante('Sergio', 22)
e.añadir_nota(7.5)
e.añadir_nota(8.0)
e.añadir_nota(6.5)
e.añadir_nota(9.0)
print(e)
print(f'\nrepr: {repr(e)}')

Solución Ejercicio 2:

class Vehiculo:
    _total_vehiculos = 0

    def __init__(self, marca, modelo, anio, kilometraje=0):
        self.marca = marca
        self.modelo = modelo
        self.anio = anio
        self.__kilometraje = 0
        self.__activo = True
        if kilometraje > 0:
            self.conducir(kilometraje)
        Vehiculo._total_vehiculos += 1

    @property
    def marca(self):
        return self.__marca

    @marca.setter
    def marca(self, valor):
        if not valor or not isinstance(valor, str):
            raise ValueError('La marca debe ser una cadena no vacía')
        self.__marca = valor.strip()

    @property
    def modelo(self):
        return self.__modelo

    @modelo.setter
    def modelo(self, valor):
        if not valor or not isinstance(valor, str):
            raise ValueError('El modelo debe ser una cadena no vacía')
        self.__modelo = valor.strip()

    @property
    def anio(self):
        return self.__anio

    @anio.setter
    def anio(self, valor):
        if not isinstance(valor, int) or not (1900 <= valor <= 2025):
            raise ValueError(f'Año no válido: {valor}')
        self.__anio = valor

    @property
    def kilometraje(self):
        return self.__kilometraje

    @property
    def activo(self):
        return self.__activo

    def conducir(self, km):
        if not self.__activo:
            raise ValueError('El vehículo está dado de baja')
        if not isinstance(km, (int, float)) or km <= 0:
            raise ValueError('Los km deben ser un número positivo')
        self.__kilometraje += km

    def dar_de_baja(self):
        self.__activo = False

    def valor_estimado(self):
        antiguedad = 2025 - self.__anio
        valor = 15000 - (antiguedad * 1000) - (self.__kilometraje * 0.01)
        return max(1000, round(valor, 2))

    @classmethod
    def total_vehiculos(cls):
        return cls._total_vehiculos

    def __str__(self):
        estado = 'activo' if self.__activo else 'baja'
        return (f'{self.__marca} {self.__modelo} ({self.__anio}) '
                f'— {self.__kilometraje:.0f} km — {estado}')

    def __repr__(self):
        return (f'Vehiculo("{self.__marca}", "{self.__modelo}", '
                f'{self.__anio}, {self.__kilometraje})')

# Uso
v1 = Vehiculo('Toyota', 'Corolla', 2018, 45000)
print(v1)
print(f'Valor estimado: {v1.valor_estimado():.2f}€\n')

v1.conducir(50000)
print(v1)
print(f'Valor estimado: {v1.valor_estimado():.2f}€\n')

v1.dar_de_baja()
print(v1)
try:
    v1.conducir(100)
except ValueError as err:
    print(f'Error: {err}\n')

v2 = Vehiculo('Honda', 'Civic', 2020)
print(f'Total vehículos: {Vehiculo.total_vehiculos()}')

Solución Ejercicio 3:

class PilaVaciaError(Exception):
    pass

class PilaLlenaError(Exception):
    pass

class Pila:
    def __init__(self, capacidad=10):
        if not isinstance(capacidad, int) or capacidad <= 0:
            raise ValueError('La capacidad debe ser un entero positivo')
        self.__capacidad = capacidad
        self.__elementos = []

    @property
    def capacidad(self):
        return self.__capacidad

    def push(self, elemento):
        if self.llena():
            raise PilaLlenaError(
                f'pila llena — capacidad máxima {self.__capacidad}')
        self.__elementos.append(elemento)

    def pop(self):
        if self.vacía():
            raise PilaVaciaError('pila vacía — no hay elementos')
        return self.__elementos.pop()

    def peek(self):
        if self.vacía():
            raise PilaVaciaError('pila vacía — no hay elementos')
        return self.__elementos[-1]

    def vacía(self):
        return len(self.__elementos) == 0

    def llena(self):
        return len(self.__elementos) >= self.__capacidad

    def __len__(self):
        return len(self.__elementos)

    def __str__(self):
        if self.vacía():
            return 'Pila vacía'
        return (f'Pila: {self.__elementos} '
                f'(tope: {self.__elementos[-1]})')

    def __repr__(self):
        return (f'Pila(capacidad={self.__capacidad}, '
                f'elementos={self.__elementos})')

# Uso
p = Pila(capacidad=3)
print(f'Pila vacía: {p.vacía()}')

p.push(10)
p.push(20)
p.push(30)
print(p)
print(f'Peek: {p.peek()}')

valor = p.pop()
print(f'Pop: {valor}')
print(p)
print(f'Tamaño: {len(p)}/{p.capacidad}\n')

print('Intentando push en pila llena...')
p.push(40)
try:
    p.push(50)
except PilaLlenaError as err:
    print(f'Error: {err}\n')

p.pop()
p.pop()
p.pop()
print('Intentando pop en pila vacía...')
try:
    p.pop()
except PilaVaciaError as err:
    print(f'Error: {err}\n')

p.push(10)
p.push(20)
print(repr(p))

Chuletario — Clases en Python

# ============================================
# CHULETARIO — Clases en Python
# Sergio Learns · sergiolearns.com
# ============================================

# DEFINIR UNA CLASE
class MiClase:
    atributo_clase = 0    # compartido por todos los objetos

    def __init__(self, param1, param2):
        self.attr1 = param1    # atributo — usa setter si existe
        self.__privado = param2    # atributo privado

    def metodo(self):
        return self.attr1

# PARÁMETRO vs ATRIBUTO
# param   → variable local de __init__, desaparece al terminar
# self.x  → atributo del objeto, persiste mientras el objeto exista
# self.x = param → crea el atributo con el valor del parámetro

# INSTANCIAR
obj = MiClase(valor1, valor2)
print(obj.attr1)
obj.metodo()

# ENCAPSULAMIENTO
class MiClase:
    def __init__(self, valor):
        self.valor = valor    # llama al setter si existe

    @property
    def valor(self):          # getter
        return self.__valor

    @valor.setter
    def valor(self, nuevo):   # setter — con validación
        if nuevo >= 0:
            self.__valor = nuevo
        else:
            raise ValueError('Valor negativo')

    @property
    def solo_lectura(self):   # sin setter → solo lectura
        return self.__valor * 2

# ATRIBUTOS PRIVADOS
self.__privado      # oculto — name mangling → _Clase__privado
self._convencion    # convención privado — accesible pero no recomendado

# MÉTODOS MÁGICOS
def __str__(self):      # para print() — legible por el usuario
    return f'...'
def __repr__(self):     # para repr() — para el programador
    return f'MiClase({self.__valor})'
def __len__(self):      # para len()
    return len(self.__lista)

# ATRIBUTOS Y MÉTODOS DE CLASE
class MiClase:
    _contador = 0                # atributo de clase

    def __init__(self):
        MiClase._contador += 1   # actualiza el atributo de clase

    @classmethod
    def total(cls):              # método de clase
        return cls._contador

MiClase.total()    # llamada desde la clase
obj.total()        # también funciona desde objeto

# ERROR TÍPICO — modificar privado directamente
obj._MiClase__privado = 999    # técnicamente funciona
                                # pero salta toda la validación
obj.valor = 999                 # BIEN — usa el setter con validación

# EXCEPCIONES EN CLASES
class ErrorMiClase(Exception):
    pass

class MiClase:
    @property
    def valor(self):
        return self.__valor

    @valor.setter
    def valor(self, nuevo):
        if nuevo < 0:
            raise ErrorMiClase('Valor negativo')
        self.__valor = nuevo

# PATRÓN COMPLETO
class ClaseCompleta:
    _contador = 0

    def __init__(self, nombre, valor):
        self.nombre = nombre    # usa setter
        self.valor = valor      # usa setter
        ClaseCompleta._contador += 1

    @property
    def nombre(self):
        return self.__nombre

    @nombre.setter
    def nombre(self, v):
        if not v: raise ValueError('Nombre vacío')
        self.__nombre = v

    @property
    def valor(self):
        return self.__valor

    @valor.setter
    def valor(self, v):
        if v < 0: raise ValueError('Negativo')
        self.__valor = v

    @classmethod
    def total(cls):
        return cls._contador

    def __str__(self):
        return f'{self.__nombre}: {self.__valor}'

    def __repr__(self):
        return f'ClaseCompleta("{self.__nombre}", {self.__valor})'

Python classes — exercises to master objects and encapsulation

Basic Level

Exercise 1 — Student class with grades and statistics

Design a Student class. Private attributes: name, age, grades list. Validate name not empty, age 16-99, grades 0-10. Methods: add_grade(grade), average(), highest(), lowest(), passed(). @property for all attributes, grades returns a copy. Implement __str__ and __repr__.

💡 Hint — modifying private attributes directly:

# WRONG — bypasses validation
s = Student('Sergio', 22)
s._Student__grades.append(-5)    # works technically — breaks encapsulation

# RIGHT — use the public method
s.add_grade(7.5)    # validates 0-10 range

Intermediate Level

Exercise 2 — Vehicle class with make, model and mileage

Private attributes: make, model, year, mileage, active. Validate make/model non-empty strings, year 1900-2025, mileage non-negative. Methods: drive(km) adds km (only if active), deactivate(), estimated_value(). Class attribute _total_vehicles, class method total_vehicles().

Value formula: base 15,000€ — 1,000€ per year old — 0.01€ per km (minimum 1,000€).

💡 Hints: drive(km) validates both km positive and vehicle active. The direct attribute mistake: v.mileage = 999999 without a setter — any value gets in.

Final Challenge

Exercise 3 — Stack class with push, pop and peek

LIFO stack with configurable maximum capacity. Methods: push(item), pop(), peek(), empty(), full(). Custom exceptions: EmptyStackError, FullStackError. Implement __len__, __str__, __repr__.

💡 Hints: Store elements in private __elements list. Capacity is read-only @property. The direct attribute mistake: stack._Stack__elements.append(x) bypasses the capacity check.

(Solutions same as Spanish version above)

Cheat sheet — Python Classes

# ============================================
# CHEAT SHEET — Python Classes
# Sergio Learns · sergiolearns.com
# ============================================

# DEFINE
class MyClass:
    class_attr = 0

    def __init__(self, param1, param2):
        self.attr = param1      # uses setter if exists
        self.__private = param2

# PARAMETER vs ATTRIBUTE
# param   → local to __init__, gone when method ends
# self.x  → belongs to object, lives as long as object does
# self.x = param → creates attribute with parameter value

# INSTANTIATE
obj = MyClass(val1, val2)

# ENCAPSULATION
class MyClass:
    def __init__(self, value):
        self.value = value    # calls setter

    @property
    def value(self):
        return self.__value

    @value.setter
    def value(self, new):
        if new >= 0: self.__value = new
        else: raise ValueError('Negative')

    @property
    def read_only(self):    # no setter → read only
        return self.__value * 2

# PRIVATE ATTRIBUTES
self.__private    # hidden — name mangling → _Class__private
self._convention  # convention only

# MAGIC METHODS
def __str__(self):   return f'...'        # print() — user-facing
def __repr__(self):  return f'MyClass()'  # repr() — programmer-facing
def __len__(self):   return len(self.__list)

# CLASS ATTRIBUTES AND METHODS
class MyClass:
    _count = 0

    def __init__(self):
        MyClass._count += 1

    @classmethod
    def total(cls):
        return cls._count

# COMMON MISTAKE
obj._MyClass__private = 999    # bypasses all validation
obj.value = 999                # RIGHT — uses setter with validation

# FULL PATTERN
class FullClass:
    _count = 0

    def __init__(self, name, value):
        self.name = name
        self.value = value
        FullClass._count += 1

    @property
    def name(self):
        return self.__name

    @name.setter
    def name(self, v):
        if not v: raise ValueError('Empty name')
        self.__name = v

    @classmethod
    def total(cls): return cls._count

    def __str__(self):  return f'{self.__name}: {self.__value}'
    def __repr__(self): return f'FullClass("{self.__name}", {self.__value})'

Publicaciones Similares

Deja una respuesta

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