Ejercicios resueltos de Python: clases, POO, NumPy y procesamiento de imágenes

Informática y Telecomunicaciones17,04 KB

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 jugador

6. 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

Descargar

PDF Word HTML