U3 - Programación Orientada a Objetos

1 Contador

class Contador:
    def __init__(self, valor_inicial=0):
        self._inicial = valor_inicial   # se guarda para poder reiniciar
        self._valor = valor_inicial     # estado actual del contador

    def incrementar(self, paso=1):
        self._valor += paso
        return self._valor

    def decrementar(self, paso=1):
        self._valor -= paso
        return self._valor

    def valor(self):
        return self._valor

    def reiniciar(self):
        self._valor = self._inicial
        return self._valor

    def __str__(self):
        return f"Contador(valor={self._valor})"


c = Contador()
c.incrementar(5)    # 5
c.decrementar(2)    # 3
c.reiniciar()
print(c.valor())

2 Magia para programadores

import math

class Pocion:
    def __init__(self, color, volumen):
        self.color = color
        self.volumen = volumen

    def mezclar(self, other):
        # NOTE: Se puede hacer de manera más sencilla
        volumen_total = self.volumen + other.volumen
        w1 = self.volumen / volumen_total
        w2 = other.volumen / volumen_total

        color = [math.ceil(c1 * w1 + c2 * w2) for c1, c2 in zip(self.color, other.color)]
        return Pocion(color=color, volumen=volumen_total)



felix_felicis = Pocion([255, 255, 255],  7)
pocion_multijugos = Pocion([51, 102, 51], 12)
nueva_pocion = felix_felicis.mezclar(pocion_multijugos)

nueva_pocion.color # Devuelve [127, 159, 127]
nueva_pocion.volumen # Devuelve 19

3 Mensaje secreto

class Cifrado:
    def __init__(self, original, alternativo):
        if len(original) != len(alternativo):
            raise ValueError(
                f"len(original) != len(alternativo) ({len(original)} vs {len(alternativo)})"
            )

        # Se usan tuplas para poder usar .index
        self.original = tuple(original) 
        self.alternativo = tuple(alternativo)


    def codificar(self, texto):
        # Version corta:
        caracteres_codificados = [
            self.alternativo[self.original.index(caracter)] for caracter in texto
        ]

        # Version larga
        caracteres_codificados = []
        for caracter in texto:
            indice = self.original.index(caracter)
            caracteres_codificados.append(self.alternativo[indice])
        return "".join(caracteres_codificados)

    def decodificar(self, texto):
        # Análogo al método anterior (pero usando generador)
        return "".join(self.original[self.alternativo.index(caracter)] for caracter in texto)


alfabeto = "abcdefghijklmnopqrstuvwxyz"
alfabeto_mezclado = "etaoinshrdlucmfwypvbgkjqxz"

mi_cifrado = Cifrado(alfabeto, alfabeto_mezclado)

mi_cifrado.codificar("abc")    # => "eta"
mi_cifrado.codificar("xyz")    # => "qxz"
mi_cifrado.codificar("aeiou")  # => "eirfg"

mi_cifrado.decodificar("eta")    # => "abc"
mi_cifrado.decodificar("qxz")    # => "xyz"
mi_cifrado.decodificar("eirfg")  # => "aeiou"

4 Real envido

class ManoDeTruco:
    def __init__(self, cartas):
        self.cartas = cartas

    def comparar_con(self, other):
        if self.puntos() >= other.puntos():
            return self
        return other

    def puntos(self):
        n_altas = len([carta for carta in self.cartas if carta >= 10])
        if n_altas == 3:
            # Si solo se tienen cartas altas, los puntos son 20.
            return 20
        elif n_altas == 2:
            # Si se tienen 2 cartas altas los puntos son 20 + la carta que no esta en ese conjunto.
            return 20 + min(self.cartas)
        elif n_altas == 1:
            # Si 1 cartas alta, los puntos son 20 + la suma de las dos cartas bajas
            return 20 + sum(sorted(self.cartas)[:2])
        # Caso contrario, es la suma de las dos cartas mas altas + 20
        return 20 + sum(sorted(self.cartas)[-2:])


mano1 = ManoDeTruco([7, 5, 6])
mano2 = ManoDeTruco([4, 11, 2])

mano1.comparar_con(mano2)

mano1.puntos()
mano2.puntos()

5 La muestra infinita

class Muestra:
    def __init__(self, iterable):
        self._datos = list(iterable) # guardo el iterable en una lista

    def agregar(self, x):
        self._datos.append(x)

    def n(self):
        return len(self._datos)

    def suma(self):
        return sum(self._datos)

    def media(self):
        n = self.n()
        return self.suma() / n

    def varianza(self, muestral=False):
        """Varianza de la muestra

        - Poblacional (por defecto): sum((xi - μ)^2) / n
        - Muestral (muestral=True):   sum((xi - x̄)^2) / (n - 1)
        """

        n = self.n()
        mu = self.media()
        ssd = sum((x - mu) ** 2 for x in self._datos)  # suma de cuadrados

        if muestral:
            denom = n - 1
        else:
            denom = n

        # otra opción
        # denom = n - 1 if muestral else n

        return ssd / denom

    @property
    def valores(self):
        """Copia inmutable de los datos (evita exponer el interno)."""
        return tuple(self._datos)



if __name__ == "__main__":
    m = Muestra([10, 12, 13, 15])
    m.agregar(20)
    print(m.n()) # 5
    print(m.suma()) # 70
    print(m.media()) # 14.0
    print(m.varianza()) # 11.6
    print(m.varianza(muestral=True)) # 14.5
    print(m.valores)  # (10, 12, 13, 15, 20)

6 ¡Orden en el laboratorio!

class Experimento:
    total_creados = 0  # atributo de clase compartido por **todas** las instancias

    def __init__(self, nombre, responsable=None):
        # Actualizamos el contador global y usamos ese valor como id
        Experimento.total_creados += 1
        self.id = Experimento.total_creados
        self.nombre = nombre
        self.responsable = responsable

    @classmethod
    def desde_dict(cls, datos):
        # crea experimento a partir de diccionario
        return cls(datos.get("nombre"), responsable=datos.get("responsable"))

    def __repr__(self):
        argumentos = (
            f"id={self.id}",
            f"nombre={self.nombre}",
            f"responsable={self.responsable}",
        )
        return f"Experimento({', '.join(argumentos)})"



e1 = Experimento("Piloto A", responsable="Dolores")
e2 = Experimento.desde_dict({"nombre": "Piloto B", "responsable": "Ana"})

print(Experimento.total_creados)  # 2
print(repr(e1))                   # Experimento(id=1, nombre='Piloto A', responsable='Dolores')
print(e2)                         # usa __repr__

7 Sensores descalibrados

import random

class Sensor:
    def __init__(self, nombre):
        self.nombre = nombre
        self._offset = 0.0  # por defecto, sin corrección

    def calibrar(self, offset):
        self._offset = float(offset)

    def leer(self):
        raise NotImplementedError("Esta clase no implementa 'leer'. Use una subclase.")


class SensorTemperatura(Sensor):
    # simula un sensor de temperatura (°C)
    def leer(self):
        base = random.uniform(18, 28)    # simulación de medición
        return base + self._offset       # corrección por calibración


class SensorHumedad(Sensor):
    # Simula un sensor de humedad (%)
    def leer(self):
        base = random.uniform(30, 70)    # simulación de medición
        return base + self._offset       # corrección por calibración


def promedio_lecturas(sensores, n=5):
    """
    Toma n lecturas de cada sensor y devuelve un dict {nombre: promedio}.
    Funciona con cualquier subclase de Sensor que implemente 'leer' (polimorfismo).
    """

    if n <= 0:
        print("n debe ser un entero positivo (>= 1).")

    resultados = {}

    # para cada sensor
    for sensor in sensores:
        # n lecturas por sensor
        acumulado = 0
        for _ in range(n):
            acumulado += sensor.leer() # sumamos los resultados de las lecturas
        resultados[sensor.nombre] = acumulado / n
    return resultados


# --- Ejemplo de uso ---
if __name__ == "__main__":
    random.seed(0)  # reproducibilidad

    t = SensorTemperatura("T1")
    h = SensorHumedad("H1")

    t.calibrar(0.5)

    promedios = promedio_lecturas([t, h], n=3)
    print(promedios)

8 Python para matemáticos

def mcd(a, b):
    # Puede ser math.gcd
    while b != 0:
        a, b = b, a % b
    return abs(a)


class Fraccion:
    def __init__(self, numerador, denominador):
        if denominador == 0:
            raise ValueError("El denominador no puede ser cero")
        self.numerador = numerador
        self.denominador = denominador
        self._simplificar()

    def _simplificar(self):
        divisor = mcd(self.numerador, self.denominador)
        self.numerador = self.numerador // divisor
        self.denominador = self.denominador // divisor

        # Normalizamos: denominador siempre positivo
        if self.denominador < 0:
            self.numerador *= -1
            self.denominador *= -1

    def __str__(self):
        return f"{self.numerador}/{self.denominador}"

    def __repr__(self):
        return f"Fraccion({self.numerador}, {self.denominador})"

    def __add__(self, other):
        if not isinstance(other, Fraccion):
            return NotImplemented
        num = self.numerador * other.denominador + other.numerador * self.denominador
        den = self.denominador * other.denominador
        return Fraccion(num, den)

    def __sub__(self, other):
        if not isinstance(other, Fraccion):
            return NotImplemented
        num = self.numerador * other.denominador - other.numerador * self.denominador
        den = self.denominador * other.denominador
        return Fraccion(num, den)

    def __mul__(self, other):
        if not isinstance(other, Fraccion):
            return NotImplemented
        num = self.numerador * other.numerador
        den = self.denominador * other.denominador
        return Fraccion(num, den)

    def __truediv__(self, other):
        if not isinstance(other, Fraccion):
            return NotImplemented

        if other.numerador == 0:
            raise ZeroDivisionError("No se puede dividir por cero")

        num = self.numerador * other.denominador
        den = self.denominador * other.numerador
        return Fraccion(num, den)


f1 = Fraccion(4, 5)
f2 = Fraccion(1, 8)

print(f1 + f2)   # 37/40
print(f1 - f2)   # 27/40
print(f1 * f2)   # 1/10
print(f1 / f2)   # 32/5

# Operandos no soportados:
f1 + 2
f1 - 2
f1 * 2
f1 / 2


f1 / Fraccion(2, 1) # Pero esto sí :')
f1 * Fraccion(5, 1) # Pero esto sí :')

9 Tiempo al tiempo ⏳

DOM = {
    1: 31,  # Enero
    2: 28,  # Febrero (29 en año bisiesto)
    3: 31,  # Marzo
    4: 30,  # Abril
    5: 31,  # Mayo
    6: 30,  # Junio
    7: 31,  # Julio
    8: 31,  # Agosto
    9: 30,  # Septiembre
    10: 31, # Octubre
    11: 30, # Noviembre
    12: 31, # Diciembre
}

class Date:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day

    @property
    def year(self):
        return self._year

    @year.setter
    def year(self, value):
        assert isinstance(value, int)
        assert value > 0
        self._year = value

    @property
    def month(self):
        return self._month

    @month.setter
    def month(self, value):
        assert isinstance(value, int)
        assert 1 <= value <= 12
        self._month = value

    @property
    def day(self):
        return self._day

    @day.setter
    def day(self, value):
        assert isinstance(value, int)
        assert 1 <= value <= Date._dias_del_mes(self.year, self.month)
        self._day = value

    @classmethod
    def from_str(cls, value):
        parts = value.split("-")
        assert len(parts) == 3
        year, month, day = map(int, map(lambda x: x.lstrip("0"), parts))

        assert year > 0
        assert 1 <= month <= 12
        assert 1 <= day <= Date._dias_del_mes(year, month)

        return cls(year=year, month=month, day=day)

    def __eq__(self, other):
        if isinstance(other, type(self)):
            return self.year == other.year and self.month == other.month and self.day == other.day
        return False

    def __neq__(self, other):
        return not self == other

    def __gt__(self, other):
        if self.year > other.year:
            return True
        elif self.year == other.year:
            if self.month > other.month:
                return True
            elif self.month == other.month:
                return self.day > other.day
        return False

    def __ge__(self, other):
        return (self > other) or (self == other)

    def __lt__(self, other):
        if self.year < other.year:
            return True
        elif self.year == other.year:
            if self.month < other.month:
                return True
            elif self.month == other.month:
                return self.day < other.day
        return False

    def __le__(self, other):
        return (self < other) or (self == other)

    def __str__(self):
        year = str(self.year).rjust(4, "0")
        month = str(self.month).rjust(2, "0")
        day = str(self.day).rjust(2, "0")

        return f"{year}-{month}-{day}"

    def __repr__(self):
        return f"Date(year={self.year}, month={self.month}, day={self.day})"

    @staticmethod
    def _es_bisiesto(year):
        # Divisible por 4,
        # excepto que sea divisible por 100,
        # salvo que tambien sea divisibles por 400
        return (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0)

    @staticmethod
    def _dias_del_mes(year, month):
        if month == 2 and Date._es_bisiesto(year):
            return 29
        return DOM[month]


d = Date(1995, 11, 6)
d
str(d)


Date.from_str("2025-05-06")

d = Date(2025, 11, 12)
d2 = Date(2025, 11, 12)
d3 = Date(2025, 11, 13)

d == d3
d > d3
d3 > d
d < d3

Date.from_str("2025-11-28")

Date(2000, 2, 29)

# Como 'Date' soporta la operación de comparación, podemos usar sorted
fechas = (Date(2022, 2, 28), Date(2025, 1, 31), Date(1816, 7, 9), Date(1810, 5, 25))
sorted(fechas)

# O max ... o min
max(fechas)
min(fechas)

# NOTA: Los métodos estáticos podrían ser tranquilamente funciones auxiliares.