Ejercicios resueltos de Python: clases, POO, NumPy y procesamiento de imágenes
Ejercicios de programación en Python
A continuación se recogen varios ejemplos de programación orientada a objetos y de uso de NumPy en Python, con clases para un juego, gestión de colas, matrículas, figuras geométricas, estadísticas de envíos y tratamiento de imágenes.
1. Juego del ahorcado
La clase Ahorcado utiliza una lista estática de palabras y una variable oculta que muestra las letras acertadas.
class Ahorcado():
LISTA = ['casa', 'perro', 'abanico'] # variable estática
def __init__(self):
self.word = Ahorcado.LISTA[random.randint(0, len(Ahorcado.LISTA) - 1)]
self.oculta = '*' * len(self.word)
self.aciertos = 0
self.fallos = 0
self.maxfallos = 4
def __repr__(self):
cadena = ''
cadena += self.oculta + '\n'
cadena += f'Fallos: {self.fallos}, Aciertos: {self.aciertos}'
return cadena
def jugar(self, char):
repeticiones = self.word.count(char)
if repeticiones > 0 and self.oculta.count(char) > 0:
self.fallos += 1
elif repeticiones > 0:
self.aciertos += repeticiones
i = 0
pos = 0
while i < repeticiones:
pos = self.word.find(char, pos)
self.oculta = self.oculta[0:pos] + char + self.oculta[pos+1:]
# print(self.oculta)
i = i + 1
pos = pos + 1
elif repeticiones == 0:
self.fallos += 1
if self.fallos >= self.maxfallos:
print('Has perdido')
if self.aciertos == len(self.word):
print('Has ganado')2. Cola de temperaturas
La clase ColaTemp hereda de list y restringe las operaciones propias de una cola (inserción intermedia, indexación y modificación por índice).
class ColaTemp(list):
def __init__(self):
super().__init__()
self.__num = 0
@property
def num(self):
return self.__num
def __len__(self):
return self.__num
def __repr__(self):
return f"ColaTemp(num={self.__num}, contenido={list(self)})"
def registrar(self, sensor: str, temperatura: float) -> None:
if not isinstance(sensor, str):
raise TypeError("El nombre del sensor debe ser un string")
if not isinstance(temperatura, (int, float)):
raise TypeError("La temperatura debe ser un número")
super().append((sensor, temperatura))
self.__num += 1
def procesar(self):
if self.__num == 0:
raise IndexError("La cola está vacía")
dato = super().pop(0)
self.__num -= 1
return dato
def insert(self, index, value):
raise NotImplementedError("No se permite insertar en posiciones intermedias en una cola")
def __getitem__(self, index):
raise NotImplementedError("No se permite indexar ni hacer slicing en una cola")
def __setitem__(self, index, value):
raise NotImplementedError("No se permite modificar elementos por índice en una cola")3. Generación de matrículas
La clase Matricula genera códigos formados por cuatro cifras y tres letras válidas, con métodos para calcular el código siguiente y anterior, comparar matrículas y guardarlas en un fichero.
class Matricula:
LETRAS_VALIDAS = "BCDFGHJKLMNPRSTVWXYZ"
ultima_matricula = None
def __init__(self):
fecha_act = datetime.now()
self.mes = fecha_act.month
self.anio = fecha_act.year
if Matricula.ultima_matricula is None:
self.codigo = Matricula.generar_matricula()
else:
self.codigo = Matricula._siguiente_matricula(Matricula.ultima_matricula)
Matricula.ultima_matricula = self.codigo
@staticmethod
def generar_matricula():
numero = random.randint(0, 9999)
letras = ''.join(random.choice(Matricula.LETRAS_VALIDAS) for _ in range(3))
return f"{numero:04d}{letras}"
@classmethod
def obtener_ultima_matricula(cls):
return cls.ultima_matricula
@classmethod
def _siguiente_matricula(cls, cod):
numero = int(cod[:4])
letras = cod[4:]
if numero < 9999:
numero += 1
return f"{numero:04d}{letras}"
numero = 0
letras_lista = list(letras)
i = 2
while i >= 0:
indice = cls.LETRAS_VALIDAS.index(letras_lista[i])
if indice < len(cls.LETRAS_VALIDAS) - 1:
letras_lista[i] = cls.LETRAS_VALIDAS[indice + 1]
break
else:
letras_lista[i] = cls.LETRAS_VALIDAS[0]
i -= 1
return f"{numero:04d}{''.join(letras_lista)}"
def _clave_orden(self):
letras = self.codigo[4:]
numero = int(self.codigo[:4])
return (letras, numero)
def __eq__(self, otro):
if not isinstance(otro, Matricula):
return NotImplemented
return self.codigo == otro.codigo
def __lt__(self, otro):
if not isinstance(otro, Matricula):
return NotImplemented
return self._clave_orden() < otro._clave_orden()
@classmethod
def _codigo_anterior(cls, cod):
numero = int(cod[:4])
letras = cod[4:]
if numero > 0:
numero -= 1
return f"{numero:04d}{letras}"
numero = 9999
letras_lista = list(letras)
i = 2
while i >= 0:
indice = cls.LETRAS_VALIDAS.index(letras_lista[i])
if indice > 0:
letras_lista[i] = cls.LETRAS_VALIDAS[indice - 1]
break
else:
letras_lista[i] = cls.LETRAS_VALIDAS[-1]
i -= 1
return f"{numero:04d}{''.join(letras_lista)}"
def matricula_anterior(self):
return Matricula._codigo_anterior(self.codigo)
@classmethod
def guardar_matriculas(cls, nombre_fichero, n):
with open(nombre_fichero, "w", encoding="utf-8") as f:
for _ in range(n):
m = cls()
f.write(f"{m.codigo};{m.mes};{m.anio}\n")
def __str__(self):
return f"Matrícula: {self.codigo[:4]}-{self.codigo[4:]} ({self.mes:02d}/{self.anio})"4. Jerarquía de figuras geométricas
Se implementa una jerarquía de herencia: Figura → Poligono → Rectangulo → Cuadrado, y Figura → Circulo, Poligono → Triangulo.
class Figura:
def __init__(self, color: str, nombre: str):
self.color = color
self.nombre = nombre
def __str__(self):
return f"{self.nombre} de color {self.color}"
class Circulo(Figura):
def __init__(self, color: str, radio: float):
super().__init__(color, "Círculo")
self.radio = radio
def area(self):
return math.pi * self.radio ** 2
def perimetro(self):
return 2 * math.pi * self.radio
def diametro(self):
return 2 * self.radio
class Poligono(Figura):
def __init__(self, color: str, nombre: str, num_lados: int):
super().__init__(color, nombre)
self.num_lados = num_lados
def __str__(self):
return f"{self.nombre} de color {self.color} con {self.num_lados} lados"
class Rectangulo(Poligono):
def __init__(self, color: str, base: float, altura: float):
super().__init__(color, "Rectángulo", 4)
self.base = base
self.altura = altura
def area(self):
return self.base * self.altura
def perimetro(self):
return 2 * (self.base + self.altura)
def es_cuadrado(self):
return self.base == self.altura
def diagonal(self):
return math.sqrt(self.base ** 2 + self.altura ** 2)
class Cuadrado(Rectangulo):
def __init__(self, color: str, lado: float):
super().__init__(color, lado, lado)
self.lado = lado
self.nombre = "Cuadrado"
def area(self):
return self.lado ** 2
class Triangulo(Poligono):
def __init__(self, color: str, lado1: float, lado2: float, lado3: float):
super().__init__(color, "Triángulo", 3)
self.lado1 = lado1
self.lado2 = lado2
self.lado3 = lado3
def area(self):
if not self.es_valido():
raise ValueError("Los lados no forman un triángulo válido")
p = self.perimetro()
s = p / 2
return math.sqrt(s * (s - self.lado1) * (s - self.lado2) * (s - self.lado3))
def perimetro(self):
if not self.es_valido():
raise ValueError("Los lados no forman un triángulo válido")
return self.lado1 + self.lado2 + self.lado3
def tipo_triangulo(self):
if not self.es_valido():
return "inválido"
if self.lado1 == self.lado2 == self.lado3:
return "equilátero"
elif self.lado1 == self.lado2 or self.lado1 == self.lado3 or self.lado2 == self.lado3:
return "isósceles"
else:
return "escaleno"
def es_valido(self):
return (
self.lado1 + self.lado2 > self.lado3 and
self.lado1 + self.lado3 > self.lado2 and
self.lado2 + self.lado3 > self.lado1
)5. Estadísticas de envíos (Player)
La clase Player registra los envíos de un jugador, calcula los puntos obtenidos y genera un resumen. Las funciones auxiliares procesan un DataFrame de Pandas.
class Player:
def __init__(self, player_id: int):
self.player_id = player_id
self.accepted_problems = dict()
self.fecha_ultimo_aceptado = None
self.unsolved = dict()
def __eq__(self, other):
return self.player_id == other.player_id
def procesar_envio(self, problem_id: str, date: datetime, verdict: str) -> None:
if problem_id in self.accepted_problems:
return
if verdict == "Accepted":
envios = 0
if problem_id in self.unsolved:
envios = self.unsolved[problem_id]
score = 10 - envios
self.fecha_ultimo_aceptado = date
self.accepted_problems[problem_id] = (date, envios, score)
self.unsolved.pop(problem_id, None)
else:
if problem_id in self.unsolved:
self.unsolved[problem_id] = self.unsolved[problem_id] + 1
else:
self.unsolved[problem_id] = 1
def aceptados(self):
return len(self.accepted_problems)
def solved_details(self):
return [(p, score) for p, (t, ev, score) in self.accepted_problems.items()]
def last(self):
return self.fecha_ultimo_aceptado
def __str__(self):
envios_no_aceptados = sum(self.unsolved.values())
puntos = sum([score for p, (time, en, score) in self.accepted_problems.items()])
return (f"Player {self.player_id}:\n"
f" Problemas resueltos: {str(self.solved_details())}\n"
f" Fecha del último envío: {self.last()}\n"
f" Envíos no aceptados: {envios_no_aceptados}\n"
f" Puntos: {puntos}")
def registrar_envio(jugador, fila):
jugador.procesar_envio(fila['problem_code'], fila['date'], fila['verdict'])
def stat_player(data: pd.DataFrame, player_id: int):
jugador = Player(player_id)
envios = data[data['player_id'] == player_id].sort_values('date')
envios.apply(lambda fila: registrar_envio(jugador, fila), axis=1)
return jugador6. Clase Hipoteca
La clase Hipoteca calcula la cuota mensual, registra los pagos realizados y el capital pendiente.
class Hipoteca:
def __init__(self, importe, tasa_interes, meses, pagos, pendientes, pago_mensual, nombre):
self._importe = importe
self._tasa_interes = tasa_interes
self._meses = meses
self._pagos = []
self._pendientes = [importe]
self._pago_mensual = pago_mensual
self._nombre = None
@staticmethod
def pago_mensual(importe, tasa_interes, meses):
return round(importe * ((tasa_interes * (1 + tasa_interes) ** meses) / (((1 + tasa_interes) ** meses) - 1)))
def realizar_pago(self):
self._pagos.append(self._pago_mensual)
reduccion = self._pago_mensual - self._pendientes[-1] * self._tasa_interes
self._pendientes.append(self._pendientes[-1] - reduccion)
def total_pagado(self):
return sum(self._pagos)7. Análisis con NumPy
Datos de lluvia
- Sustituir -1 por 0:
lluvia[lluvia == -1] = 0 - Día con mayor precipitación:
idx_max = np.argmax(lluvia),fechas[idx_max],lluvia[idx_max] - Diferencia con el día siguiente:
dif = lluvia[1:] - lluvia[:-1]
Colección de cromos
juan = np.loadtxt("juan.txt", dtype=int)- Cromos diferentes:
dif_juan = juan.shape[0] - Cromos totales contando repetidos:
tot_juan = np.sum(juan[:, 1]) - Cromos que aparecen en ambas colecciones:
comunes = np.intersect1d(juan[:, 0], pedro[:, 0]) - ¿Forman la colección completa?
union = np.union1d(juan[:, 0], pedro[:, 0]),len(union) - Cromos que faltan:
completa = np.arange(1, 201),faltan = np.setdiff1d(completa, union) - Cromo más repetido de Juan:
mas_rep = juan.max(axis=0)[1],juan[juan[:, 1] == mas_rep] - Cromos con menos de 10 unidades:
cond = pedro[:, 1] < 10,cromos = pedro[cond, 0]
Precio del petróleo
- Primera vez que el precio superó 40 euros:
idx_40 = np.argmax(precio > 40),fecha_40 = f"{int(anio[idx_40])}-{int(mes[idx_40]):02d}" - Datos de Europa y África entre 1956 y 1960:
europe_africa_1956_1960 = world[1:6, [1, 5]]
Humedad y temperatura
- Clasificar cada punto de la cuadrícula en zona 1, 2 o 3 según su humedad:
zonas = np.ones(hum.shape)
zona1 = hum <= 28
zona2 = (hum > 28) & (hum < 55)
zona3 = hum > 55
zonas[zona1] = 1
zonas[zona2] = 2
zonas[zona3] = 3
# ¿Hay algún punto en la zona 3? ¿Cuántos en la zona 2?
hay_zona3 = np.any(zonas == 3)
num_zona2 = np.sum(zonas == 2)8. Procesamiento de imágenes
La clase Picture carga una imagen, la convierte a escala de grises y permite aplicar transformaciones como negativo, rotación, cambio de brillo, marco y pixelado.
class Picture:
def __init__(self, filepath: str):
self.color = np.array(Image.open(filepath))
self.gray = Picture.to_grayscale(self.color)
self.gray_framed = Picture.imagen_marco(self.gray)
@staticmethod
def to_grayscale(image: np.ndarray) -> np.ndarray:
gray = image
if len(image.shape) == 3:
gray = np.mean(image, axis=2).astype(np.uint8)
return gray
def show_images(self):
figure, g = plt.subplots(nrows=1, ncols=3, figsize=(8, 4))
g[0].set_title('Original')
g[0].imshow(self.color)
g[0].axis('off')
# ---------------------------------
t = Picture("rana.jpg")
t.show_images()
@staticmethod
def imagen_marco(gray):
nueva = gray.copy()
nueva[0:10, :] = 0
nueva[-10:, :] = 0
nueva[:, :10] = 0
nueva[:, -10:] = 0
return nueva
def negativo(self):
self.gray = 255 - self.gray
def rotar(self, grados):
if grados == 90:
self.gray = self.gray.T[-1::-1, :]
elif grados == 180:
self.gray = self.gray.T[-1::-1, :]
self.gray = self.gray.T[-1::-1, :]
elif grados == 270:
# igual 3 veces
pass
def cambiar_brillo(self, n: int):
temp = self.gray.astype(np.int16) + n
temp = np.where(temp < 0, 0, temp)
temp = np.where(temp > 255, 255, temp)
self.gray = temp.astype(np.uint8)
def pixelar(self, x1, y1, x2, y2, tamaño=20):
for fila in range(x1, x2, tamaño):
for columna in range(y1, y2, tamaño):
trozo = self.gray[fila:fila+tamaño, columna:columna+tamaño]
media = trozo.mean()
trozo[:, :] = media