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.
Tabla de Contenidos
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,edady una lista denotas - 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
@propertyparanombre,edadynotas(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,kilometrajeyactivo - 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_vehiculosy método de clasetotal_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 quekmsea positivo y que el vehículo esté activo, lanzaValueErroren ambos casos valor_estimado()usa__anioy__kilometrajedirectamente, 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 pilapop()— elimina y devuelve el elemento de encimapeek()— devuelve el elemento de encima sin eliminarlovacía()— devuelve True si la pila está vacíallena()— devuelve True si la pila está llena
Define estas excepciones propias:
PilaVaciaError— cuando intentas hacer pop o peek en una pila vacíaPilaLlenaError— 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,
@propertysin setter pushdebe comprobar si está llena antes de añadirpopypeekdeben 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})'
