Ir al contenido

Programación Orientada a Objetos - Guía Completa

Publicado: a las  10:00 a. m.

Programación Orientada a Objetos - Guía Completa

🎯 Introducción: ¿Por qué POO?

La Programación Orientada a Objetos (POO) es uno de los paradigmas más importantes y utilizados en la industria del software. No es solo una forma de escribir código, es una manera de pensar sobre cómo resolver problemas complejos.

Imaginate que tenés que diseñar un sistema de control para una planta industrial. Tenés sensores, actuadores, válvulas, motores, PLCs, pantallas HMI… ¿Cómo organizás todo ese código? ¿Cómo hacés para que sea fácil de mantener, extender y reutilizar?

La respuesta es POO. 🚀

En este artículo vas a aprender a pensar en objetos, a diseñar sistemas complejos de manera elegante, y a escribir código que tus compañeros (y vos mismo en 6 meses) puedan entender fácilmente.


📦 Conceptos fundamentales

¿Qué es un objeto?

Un objeto es una entidad que representa algo del mundo real o conceptual. Tiene:

¿Qué es una clase?

Una clase es el molde, la plantilla, el plano arquitectónico para crear objetos. Define:

Analogía: Si una clase es el plano de una casa, un objeto es la casa construida. Podés construir muchas casas (objetos) a partir del mismo plano (clase).


🏭 Ejemplo industrial: Sistema de control de temperatura

Vamos a crear un sistema para controlar la temperatura de diferentes zonas en una planta industrial. Este es un ejemplo real de lo que podrías programar en tu carrera.

Versión básica: Primera clase

class SensorTemperatura:
    """
    Representa un sensor de temperatura en una planta industrial
    """

    def __init__(self, id_sensor, ubicacion, temp_min=0, temp_max=100):
        """
        Constructor: se ejecuta cuando creamos un nuevo objeto

        Args:
            id_sensor: Identificador único del sensor
            ubicacion: Dónde está instalado el sensor
            temp_min: Temperatura mínima permitida
            temp_max: Temperatura máxima permitida
        """
        # Atributos de instancia: cada objeto tiene sus propios valores
        self.id_sensor = id_sensor
        self.ubicacion = ubicacion
        self.temp_min = temp_min
        self.temp_max = temp_max
        self.temperatura_actual = 0
        self.activo = True
        self.historial = []

    def leer_temperatura(self, nueva_temp):
        """
        Registra una nueva lectura de temperatura
        """
        if self.activo:
            self.temperatura_actual = nueva_temp
            self.historial.append(nueva_temp)
            return True
        return False

    def verificar_rango(self):
        """
        Verifica si la temperatura está dentro del rango permitido
        """
        if self.temperatura_actual < self.temp_min:
            return "ALERTA_BAJA"
        elif self.temperatura_actual > self.temp_max:
            return "ALERTA_ALTA"
        else:
            return "NORMAL"

    def obtener_promedio(self):
        """
        Calcula el promedio de temperaturas registradas
        """
        if self.historial:
            return sum(self.historial) / len(self.historial)
        return 0

    def __str__(self):
        """
        Método especial: define cómo se representa el objeto como string
        """
        estado = "🟢 Activo" if self.activo else "🔴 Inactivo"
        return f"Sensor {self.id_sensor} ({self.ubicacion}): {self.temperatura_actual}°C - {estado}"

    def __repr__(self):
        """
        Método especial: representación técnica del objeto
        """
        return f"SensorTemperatura(id={self.id_sensor}, ubicacion='{self.ubicacion}')"

# Creación de objetos (instancias)
sensor1 = SensorTemperatura("TEMP-001", "Caldera Principal", temp_min=60, temp_max=90)
sensor2 = SensorTemperatura("TEMP-002", "Sala de Máquinas", temp_min=15, temp_max=30)
sensor3 = SensorTemperatura("TEMP-003", "Tanque de Agua", temp_min=5, temp_max=25)

# Uso de los objetos
sensor1.leer_temperatura(75)
sensor1.leer_temperatura(78)
sensor1.leer_temperatura(82)

sensor2.leer_temperatura(22)
sensor2.leer_temperatura(28)

print(sensor1)  # Usa __str__
print(f"Estado: {sensor1.verificar_rango()}")
print(f"Promedio: {sensor1.obtener_promedio():.2f}°C")
print(f"Historial: {sensor1.historial}")

Salida:

Sensor TEMP-001 (Caldera Principal): 82°C - 🟢 Activo
Estado: NORMAL
Promedio: 78.33°C
Historial: [75, 78, 82]

🏗️ Los 4 Pilares de la POO

1. 🔒 Encapsulamiento

El encapsulamiento es proteger los datos internos de un objeto y controlar cómo se accede a ellos. Es como tener una caja fuerte: no dejás que cualquiera meta la mano, sino que proporcionás métodos seguros para interactuar.

Niveles de acceso en Python:

class ControladorPLC:
    """
    Controlador PLC con encapsulamiento de datos críticos
    """

    # Atributo de clase: compartido por todas las instancias
    fabricante = "Siemens"

    def __init__(self, modelo, direccion_ip):
        self.modelo = modelo              # Público
        self._direccion_ip = direccion_ip # Protegido
        self.__clave_acceso = "admin123"  # Privado
        self.__intentos_fallidos = 0      # Privado

    # Getter: método para obtener un valor privado
    def obtener_direccion_ip(self):
        return self._direccion_ip

    # Setter: método para modificar un valor privado con validación
    def cambiar_clave(self, clave_actual, clave_nueva):
        if clave_actual == self.__clave_acceso:
            if len(clave_nueva) >= 8:
                self.__clave_acceso = clave_nueva
                self.__intentos_fallidos = 0
                return "✅ Clave cambiada exitosamente"
            else:
                return "❌ La clave debe tener al menos 8 caracteres"
        else:
            self.__intentos_fallidos += 1
            if self.__intentos_fallidos >= 3:
                return "🚨 BLOQUEADO: Demasiados intentos fallidos"
            return "❌ Clave actual incorrecta"

    def autenticar(self, clave):
        """Método público para autenticación"""
        return clave == self.__clave_acceso

    # Property: forma pythónica de crear getters/setters
    @property
    def direccion_ip(self):
        """Getter como propiedad"""
        return self._direccion_ip

    @direccion_ip.setter
    def direccion_ip(self, nueva_ip):
        """Setter con validación"""
        # Validación simple de formato IP
        partes = nueva_ip.split('.')
        if len(partes) == 4 and all(p.isdigit() and 0 <= int(p) <= 255 for p in partes):
            self._direccion_ip = nueva_ip
        else:
            raise ValueError("❌ Formato de IP inválido")

# Uso
plc = ControladorPLC("S7-1200", "192.168.1.100")

# Acceso público
print(f"Modelo: {plc.modelo}")
print(f"Fabricante: {plc.fabricante}")

# Acceso mediante property
print(f"IP: {plc.direccion_ip}")
plc.direccion_ip = "192.168.1.101"  # Usa el setter
print(f"Nueva IP: {plc.direccion_ip}")

# Intento de acceso directo a atributo privado (no recomendado)
# print(plc.__clave_acceso)  # ❌ Error: AttributeError

# Forma correcta: mediante métodos públicos
print(plc.cambiar_clave("admin123", "nueva_clave_segura_2024"))
print(plc.autenticar("nueva_clave_segura_2024"))  # True

¿Por qué es importante el encapsulamiento?


2. 🧬 Herencia

La herencia permite crear nuevas clases basadas en clases existentes, reutilizando y extendiendo funcionalidad.

class DispositivoIndustrial:
    """
    Clase base para todos los dispositivos industriales
    """

    def __init__(self, id_dispositivo, ubicacion):
        self.id_dispositivo = id_dispositivo
        self.ubicacion = ubicacion
        self.activo = False
        self.horas_operacion = 0

    def encender(self):
        self.activo = True
        return f"✅ {self.id_dispositivo} encendido"

    def apagar(self):
        self.activo = False
        return f"🔴 {self.id_dispositivo} apagado"

    def registrar_horas(self, horas):
        self.horas_operacion += horas

    def necesita_mantenimiento(self):
        """Cada 1000 horas de operación"""
        return self.horas_operacion >= 1000

# Herencia: Sensor hereda de DispositivoIndustrial
class Sensor(DispositivoIndustrial):
    """
    Sensor genérico que hereda de DispositivoIndustrial
    """

    def __init__(self, id_dispositivo, ubicacion, unidad_medida):
        # Llamamos al constructor de la clase padre
        super().__init__(id_dispositivo, ubicacion)
        self.unidad_medida = unidad_medida
        self.valor_actual = 0
        self.calibrado = True

    def leer_valor(self, nuevo_valor):
        if self.activo and self.calibrado:
            self.valor_actual = nuevo_valor
            return True
        return False

    def calibrar(self):
        self.calibrado = True
        return f"🔧 {self.id_dispositivo} calibrado"

# Herencia: Actuador hereda de DispositivoIndustrial
class Actuador(DispositivoIndustrial):
    """
    Actuador genérico que hereda de DispositivoIndustrial
    """

    def __init__(self, id_dispositivo, ubicacion, potencia_max):
        super().__init__(id_dispositivo, ubicacion)
        self.potencia_max = potencia_max
        self.potencia_actual = 0

    def ajustar_potencia(self, porcentaje):
        if 0 <= porcentaje <= 100 and self.activo:
            self.potencia_actual = (porcentaje / 100) * self.potencia_max
            return f"⚡ Potencia ajustada a {porcentaje}%"
        return "❌ No se puede ajustar potencia"

# Herencia multinivel: SensorTemperatura hereda de Sensor
class SensorTemperatura(Sensor):
    """
    Sensor especializado en temperatura
    """

    def __init__(self, id_dispositivo, ubicacion, temp_min=-50, temp_max=150):
        super().__init__(id_dispositivo, ubicacion, "°C")
        self.temp_min = temp_min
        self.temp_max = temp_max

    def verificar_rango(self):
        if self.valor_actual < self.temp_min:
            return "⚠️ TEMPERATURA BAJA"
        elif self.valor_actual > self.temp_max:
            return "🚨 TEMPERATURA ALTA"
        return "✅ NORMAL"

# Herencia multinivel: Válvula hereda de Actuador
class Valvula(Actuador):
    """
    Válvula controlada que hereda de Actuador
    """

    def __init__(self, id_dispositivo, ubicacion):
        super().__init__(id_dispositivo, ubicacion, potencia_max=100)
        self.apertura = 0  # 0-100%

    def abrir(self, porcentaje=100):
        if self.activo:
            self.apertura = min(porcentaje, 100)
            self.ajustar_potencia(self.apertura)
            return f"🔓 Válvula abierta al {self.apertura}%"
        return "❌ Válvula no activa"

    def cerrar(self):
        self.apertura = 0
        self.ajustar_potencia(0)
        return "🔒 Válvula cerrada"

# Uso de la jerarquía de herencia
sensor_temp = SensorTemperatura("TEMP-001", "Caldera", temp_min=60, temp_max=90)
valvula = Valvula("VALV-001", "Línea principal")

# Métodos heredados de DispositivoIndustrial
print(sensor_temp.encender())
print(valvula.encender())

# Métodos específicos de cada clase
sensor_temp.leer_valor(75)
print(f"Temperatura: {sensor_temp.valor_actual}{sensor_temp.unidad_medida}")
print(sensor_temp.verificar_rango())

print(valvula.abrir(50))
print(f"Apertura actual: {valvula.apertura}%")

# Verificar tipo de objeto
print(f"¿sensor_temp es un Sensor? {isinstance(sensor_temp, Sensor)}")  # True
print(f"¿sensor_temp es un DispositivoIndustrial? {isinstance(sensor_temp, DispositivoIndustrial)}")  # True

Ventajas de la herencia:


3. 🎭 Polimorfismo

El polimorfismo permite que diferentes objetos respondan al mismo mensaje de formas distintas. Es como tener un control remoto universal: el botón “encender” funciona con la TV, el aire acondicionado y el equipo de música, pero cada uno hace algo diferente.

from abc import ABC, abstractmethod
import math

class FormaGeometrica(ABC):
    """
    Clase abstracta base para formas geométricas
    """

    @abstractmethod
    def calcular_area(self):
        """Método abstracto: debe ser implementado por las subclases"""
        pass

    @abstractmethod
    def calcular_perimetro(self):
        """Método abstracto: debe ser implementado por las subclases"""
        pass

    def describir(self):
        """Método concreto: puede ser usado por todas las subclases"""
        return f"{self.__class__.__name__}: Área={self.calcular_area():.2f}, Perímetro={self.calcular_perimetro():.2f}"

class Rectangulo(FormaGeometrica):
    def __init__(self, base, altura):
        self.base = base
        self.altura = altura

    def calcular_area(self):
        return self.base * self.altura

    def calcular_perimetro(self):
        return 2 * (self.base + self.altura)

class Circulo(FormaGeometrica):
    def __init__(self, radio):
        self.radio = radio

    def calcular_area(self):
        return math.pi * self.radio ** 2

    def calcular_perimetro(self):
        return 2 * math.pi * self.radio

class Triangulo(FormaGeometrica):
    def __init__(self, lado1, lado2, lado3):
        self.lado1 = lado1
        self.lado2 = lado2
        self.lado3 = lado3

    def calcular_area(self):
        # Fórmula de Herón
        s = self.calcular_perimetro() / 2
        return math.sqrt(s * (s - self.lado1) * (s - self.lado2) * (s - self.lado3))

    def calcular_perimetro(self):
        return self.lado1 + self.lado2 + self.lado3

# Polimorfismo en acción
formas = [
    Rectangulo(10, 5),
    Circulo(7),
    Triangulo(3, 4, 5),
    Rectangulo(8, 8)
]

# Mismo método, diferentes comportamientos
print("📐 CÁLCULO DE ÁREAS Y PERÍMETROS")
print("=" * 50)

for forma in formas:
    # Polimorfismo: cada objeto responde de manera diferente
    print(forma.describir())
    print(f"  → Área: {forma.calcular_area():.2f}")
    print(f"  → Perímetro: {forma.calcular_perimetro():.2f}")
    print()

# Función que trabaja con cualquier FormaGeometrica (polimorfismo)
def calcular_area_total(formas_lista):
    """
    Calcula el área total de una lista de formas
    No le importa qué tipo específico de forma es cada elemento
    """
    return sum(forma.calcular_area() for forma in formas_lista)

print(f"Área total de todas las formas: {calcular_area_total(formas):.2f}")

Ejemplo industrial: Sistema de notificaciones

class SistemaNotificacion(ABC):
    """Clase base abstracta para sistemas de notificación"""

    @abstractmethod
    def enviar(self, mensaje):
        pass

class NotificacionEmail(SistemaNotificacion):
    def __init__(self, destinatario):
        self.destinatario = destinatario

    def enviar(self, mensaje):
        return f"📧 Email enviado a {self.destinatario}: {mensaje}"

class NotificacionSMS(SistemaNotificacion):
    def __init__(self, numero):
        self.numero = numero

    def enviar(self, mensaje):
        return f"📱 SMS enviado a {self.numero}: {mensaje}"

class NotificacionSCADA(SistemaNotificacion):
    def __init__(self, pantalla_id):
        self.pantalla_id = pantalla_id

    def enviar(self, mensaje):
        return f"🖥️ Alerta en SCADA (Pantalla {self.pantalla_id}): {mensaje}"

class GestorAlertas:
    """Gestor que usa polimorfismo para enviar alertas"""

    def __init__(self):
        self.canales = []

    def agregar_canal(self, canal):
        self.canales.append(canal)

    def enviar_alerta(self, mensaje):
        """Envía la alerta por todos los canales configurados"""
        resultados = []
        for canal in self.canales:
            # Polimorfismo: cada canal implementa enviar() a su manera
            resultados.append(canal.enviar(mensaje))
        return resultados

# Uso
gestor = GestorAlertas()
gestor.agregar_canal(NotificacionEmail("operador@planta.com"))
gestor.agregar_canal(NotificacionSMS("+54-2966-123456"))
gestor.agregar_canal(NotificacionSCADA("HMI-001"))

# Una sola llamada, múltiples comportamientos
alertas = gestor.enviar_alerta("⚠️ Temperatura de caldera fuera de rango")
for alerta in alertas:
    print(alerta)

4. 🎨 Abstracción

La abstracción es ocultar la complejidad y mostrar solo lo esencial. Es como manejar un auto: no necesitás saber cómo funciona el motor internamente, solo necesitás saber usar el volante, los pedales y la palanca de cambios.

from abc import ABC, abstractmethod
from datetime import datetime

class ProtocoloComunicacion(ABC):
    """
    Abstracción de un protocolo de comunicación industrial
    """

    @abstractmethod
    def conectar(self, direccion):
        """Establece conexión con el dispositivo"""
        pass

    @abstractmethod
    def desconectar(self):
        """Cierra la conexión"""
        pass

    @abstractmethod
    def leer_registro(self, registro):
        """Lee un registro del dispositivo"""
        pass

    @abstractmethod
    def escribir_registro(self, registro, valor):
        """Escribe un valor en un registro"""
        pass

class ModbusTCP(ProtocoloComunicacion):
    """Implementación concreta: Modbus TCP"""

    def __init__(self):
        self.conectado = False
        self.direccion = None

    def conectar(self, direccion):
        # Aquí iría la lógica real de conexión Modbus TCP
        self.direccion = direccion
        self.conectado = True
        return f"✅ Conectado vía Modbus TCP a {direccion}"

    def desconectar(self):
        self.conectado = False
        return "🔌 Desconectado de Modbus TCP"

    def leer_registro(self, registro):
        if not self.conectado:
            return None
        # Simulación de lectura
        return f"Valor del registro {registro} vía Modbus TCP"

    def escribir_registro(self, registro, valor):
        if not self.conectado:
            return False
        # Simulación de escritura
        return f"✍️ Escrito {valor} en registro {registro} vía Modbus TCP"

class OPC_UA(ProtocoloComunicacion):
    """Implementación concreta: OPC UA"""

    def __init__(self):
        self.sesion_activa = False
        self.servidor = None

    def conectar(self, direccion):
        self.servidor = direccion
        self.sesion_activa = True
        return f"✅ Sesión OPC UA iniciada con {direccion}"

    def desconectar(self):
        self.sesion_activa = False
        return "🔌 Sesión OPC UA cerrada"

    def leer_registro(self, registro):
        if not self.sesion_activa:
            return None
        return f"Valor del nodo {registro} vía OPC UA"

    def escribir_registro(self, registro, valor):
        if not self.sesion_activa:
            return False
        return f"✍️ Escrito {valor} en nodo {registro} vía OPC UA"

class ControladorProceso:
    """
    Controlador que usa abstracción para trabajar con cualquier protocolo
    """

    def __init__(self, protocolo: ProtocoloComunicacion):
        # Recibe una abstracción, no una implementación concreta
        self.protocolo = protocolo
        self.log = []

    def iniciar(self, direccion):
        resultado = self.protocolo.conectar(direccion)
        self.log.append(f"[{datetime.now()}] {resultado}")
        return resultado

    def detener(self):
        resultado = self.protocolo.desconectar()
        self.log.append(f"[{datetime.now()}] {resultado}")
        return resultado

    def leer_temperatura(self):
        # No le importa QUÉ protocolo es, solo que tenga el método leer_registro
        temp = self.protocolo.leer_registro("temperatura")
        self.log.append(f"[{datetime.now()}] Lectura: {temp}")
        return temp

    def ajustar_setpoint(self, valor):
        resultado = self.protocolo.escribir_registro("setpoint", valor)
        self.log.append(f"[{datetime.now()}] {resultado}")
        return resultado

# Uso: La abstracción permite cambiar de protocolo fácilmente
print("🔧 USANDO MODBUS TCP")
print("=" * 50)
controlador1 = ControladorProceso(ModbusTCP())
print(controlador1.iniciar("192.168.1.100"))
print(controlador1.leer_temperatura())
print(controlador1.ajustar_setpoint(75))
print(controlador1.detener())

print("\n🔧 USANDO OPC UA")
print("=" * 50)
controlador2 = ControladorProceso(OPC_UA())
print(controlador2.iniciar("opc.tcp://192.168.1.200:4840"))
print(controlador2.leer_temperatura())
print(controlador2.ajustar_setpoint(75))
print(controlador2.detener())

# El código del controlador NO cambia, solo cambiamos el protocolo

🔄 Composición vs Herencia

“Favor composition over inheritance” es un principio importante en POO. A veces es mejor tener un objeto que ser un objeto.

# ❌ MAL: Herencia excesiva
class Motor:
    def arrancar(self):
        return "Motor arrancado"

class Ruedas:
    def girar(self):
        return "Ruedas girando"

# Esto NO tiene sentido: un auto no "es un" motor ni "es unas" ruedas
# class Auto(Motor, Ruedas):  # Herencia múltiple confusa
#     pass

# ✅ BIEN: Composición
class Motor:
    def __init__(self, potencia):
        self.potencia = potencia
        self.encendido = False

    def arrancar(self):
        self.encendido = True
        return f"Motor de {self.potencia}HP arrancado"

    def apagar(self):
        self.encendido = False
        return "Motor apagado"

class Rueda:
    def __init__(self, tamaño):
        self.tamaño = tamaño
        self.presion = 32  # PSI

class Auto:
    """
    Un auto TIENE un motor y TIENE ruedas (composición)
    """
    def __init__(self, marca, modelo, potencia_motor):
        self.marca = marca
        self.modelo = modelo
        # Composición: el auto contiene otros objetos
        self.motor = Motor(potencia_motor)
        self.ruedas = [Rueda(17) for _ in range(4)]
        self.velocidad = 0

    def arrancar(self):
        return self.motor.arrancar()

    def acelerar(self, incremento):
        if self.motor.encendido:
            self.velocidad += incremento
            return f"Acelerando... Velocidad: {self.velocidad} km/h"
        return "Primero arrancá el motor"

    def verificar_ruedas(self):
        return [f"Rueda {i+1}: {rueda.presion} PSI" for i, rueda in enumerate(self.ruedas)]

# Uso
auto = Auto("Toyota", "Corolla", 140)
print(auto.arrancar())
print(auto.acelerar(50))
print(auto.verificar_ruedas())

Ejemplo industrial: Sistema SCADA con composición

class BaseDatos:
    """Componente para almacenamiento de datos"""
    def __init__(self, nombre_bd):
        self.nombre_bd = nombre_bd
        self.datos = {}

    def guardar(self, clave, valor):
        self.datos[clave] = valor
        return f"💾 Guardado en {self.nombre_bd}: {clave} = {valor}"

    def leer(self, clave):
        return self.datos.get(clave)

class InterfazGrafica:
    """Componente para visualización"""
    def __init__(self, resolucion):
        self.resolucion = resolucion
        self.pantallas = []

    def mostrar_valor(self, etiqueta, valor):
        return f"🖥️ [{self.resolucion}] {etiqueta}: {valor}"

class SistemaAlarmas:
    """Componente para gestión de alarmas"""
    def __init__(self):
        self.alarmas_activas = []

    def generar_alarma(self, mensaje, prioridad):
        alarma = {"mensaje": mensaje, "prioridad": prioridad, "timestamp": datetime.now()}
        self.alarmas_activas.append(alarma)
        return f"🚨 ALARMA [{prioridad}]: {mensaje}"

class SistemaSCADA:
    """
    Sistema SCADA que usa composición para integrar componentes
    """
    def __init__(self, nombre):
        self.nombre = nombre
        # Composición: SCADA TIENE estos componentes
        self.base_datos = BaseDatos(f"{nombre}_DB")
        self.interfaz = InterfazGrafica("1920x1080")
        self.alarmas = SistemaAlarmas()
        self.sensores = []

    def agregar_sensor(self, sensor):
        self.sensores.append(sensor)

    def actualizar_sensor(self, id_sensor, valor):
        # Guarda en BD
        print(self.base_datos.guardar(id_sensor, valor))

        # Muestra en interfaz
        print(self.interfaz.mostrar_valor(id_sensor, valor))

        # Verifica alarmas
        if valor > 100:  # Ejemplo simple
            print(self.alarmas.generar_alarma(f"{id_sensor} fuera de rango: {valor}", "ALTA"))

    def reporte_estado(self):
        print(f"\n📊 REPORTE DEL SISTEMA {self.nombre}")
        print("=" * 50)
        print(f"Sensores registrados: {len(self.sensores)}")
        print(f"Datos en BD: {len(self.base_datos.datos)}")
        print(f"Alarmas activas: {len(self.alarmas.alarmas_activas)}")

# Uso
scada = SistemaSCADA("PlantaCalafate")
scada.actualizar_sensor("TEMP-001", 75)
scada.actualizar_sensor("PRES-002", 105)
scada.reporte_estado()

🎯 Métodos y atributos especiales

Python tiene métodos especiales (también llamados “magic methods” o “dunder methods”) que permiten personalizar el comportamiento de tus objetos.

class Vector2D:
    """
    Representa un vector en 2D con métodos especiales
    """

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        """Representación legible para humanos"""
        return f"Vector({self.x}, {self.y})"

    def __repr__(self):
        """Representación técnica"""
        return f"Vector2D(x={self.x}, y={self.y})"

    def __add__(self, otro):
        """Sobrecarga del operador +"""
        return Vector2D(self.x + otro.x, self.y + otro.y)

    def __sub__(self, otro):
        """Sobrecarga del operador -"""
        return Vector2D(self.x - otro.x, self.y - otro.y)

    def __mul__(self, escalar):
        """Sobrecarga del operador * (multiplicación por escalar)"""
        return Vector2D(self.x * escalar, self.y * escalar)

    def __eq__(self, otro):
        """Sobrecarga del operador =="""
        return self.x == otro.x and self.y == otro.y

    def __len__(self):
        """Sobrecarga de len()"""
        return int((self.x**2 + self.y**2)**0.5)

    def __getitem__(self, indice):
        """Permite acceso por índice: vector[0], vector[1]"""
        if indice == 0:
            return self.x
        elif indice == 1:
            return self.y
        else:
            raise IndexError("Índice fuera de rango")

    def __call__(self):
        """Hace que el objeto sea callable"""
        return f"Magnitud: {len(self)}"

# Uso de métodos especiales
v1 = Vector2D(3, 4)
v2 = Vector2D(1, 2)

print(v1)                    # __str__
print(repr(v1))              # __repr__
print(v1 + v2)               # __add__
print(v1 - v2)               # __sub__
print(v1 * 3)                # __mul__
print(v1 == v2)              # __eq__
print(len(v1))               # __len__
print(v1[0], v1[1])          # __getitem__
print(v1())                  # __call__

🏆 Patrones de diseño (Introducción)

Los patrones de diseño son soluciones probadas a problemas comunes en POO. Acá te presento algunos básicos:

1. Singleton (Una sola instancia)

class ConfiguracionPlanta:
    """
    Singleton: Solo puede existir UNA instancia de configuración
    """
    _instancia = None

    def __new__(cls):
        if cls._instancia is None:
            cls._instancia = super().__new__(cls)
            cls._instancia.inicializar()
        return cls._instancia

    def inicializar(self):
        self.nombre_planta = "Planta El Calafate"
        self.temperatura_maxima = 100
        self.presion_maxima = 10

# Siempre obtenemos la misma instancia
config1 = ConfiguracionPlanta()
config2 = ConfiguracionPlanta()

print(config1 is config2)  # True - ¡Son el mismo objeto!
config1.nombre_planta = "Planta Modificada"
print(config2.nombre_planta)  # "Planta Modificada"

2. Factory (Fábrica de objetos)

class FabricaSensores:
    """
    Factory: Crea diferentes tipos de sensores según el tipo solicitado
    """

    @staticmethod
    def crear_sensor(tipo, id_sensor, ubicacion):
        if tipo == "temperatura":
            return SensorTemperatura(id_sensor, ubicacion)
        elif tipo == "presion":
            return SensorPresion(id_sensor, ubicacion)
        elif tipo == "nivel":
            return SensorNivel(id_sensor, ubicacion)
        else:
            raise ValueError(f"Tipo de sensor desconocido: {tipo}")

class SensorPresion(Sensor):
    def __init__(self, id_sensor, ubicacion):
        super().__init__(id_sensor, ubicacion, "bar")

class SensorNivel(Sensor):
    def __init__(self, id_sensor, ubicacion):
        super().__init__(id_sensor, ubicacion, "m")

# Uso
sensores = [
    FabricaSensores.crear_sensor("temperatura", "TEMP-001", "Caldera"),
    FabricaSensores.crear_sensor("presion", "PRES-001", "Línea A"),
    FabricaSensores.crear_sensor("nivel", "NIV-001", "Tanque 1")
]

for sensor in sensores:
    print(f"{sensor.id_sensor}: {sensor.unidad_medida}")

3. Observer (Observador)

class SujetoObservable:
    """
    Patrón Observer: Notifica a múltiples observadores cuando cambia el estado
    """

    def __init__(self):
        self._observadores = []
        self._estado = None

    def agregar_observador(self, observador):
        self._observadores.append(observador)

    def eliminar_observador(self, observador):
        self._observadores.remove(observador)

    def notificar_observadores(self):
        for observador in self._observadores:
            observador.actualizar(self._estado)

    def cambiar_estado(self, nuevo_estado):
        self._estado = nuevo_estado
        self.notificar_observadores()

class ObservadorAlarma:
    def __init__(self, nombre):
        self.nombre = nombre

    def actualizar(self, estado):
        print(f"🚨 [{self.nombre}] Alerta: Estado cambió a {estado}")

class ObservadorRegistro:
    def __init__(self):
        self.historial = []

    def actualizar(self, estado):
        self.historial.append(estado)
        print(f"📝 Registrado en log: {estado}")

# Uso
sensor = SujetoObservable()
sensor.agregar_observador(ObservadorAlarma("Sistema de Alarmas"))
sensor.agregar_observador(ObservadorRegistro())

sensor.cambiar_estado("Temperatura: 85°C")
sensor.cambiar_estado("Temperatura: 95°C")

🎮 Ejercicios prácticos

Ejercicio 1: Sistema de control de tanques

Creá un sistema para controlar tanques de agua con las siguientes características:

class Tanque:
    """
    Implementá esta clase con:
    - Atributos: capacidad_maxima, nivel_actual, ubicacion
    - Métodos:
        - llenar(cantidad): agrega agua al tanque
        - vaciar(cantidad): quita agua del tanque
        - obtener_porcentaje(): retorna el % de llenado
        - necesita_recarga(): retorna True si está por debajo del 20%
    """
    pass

# Tu código acá

Ejercicio 2: Jerarquía de dispositivos

Creá una jerarquía de clases para dispositivos de una planta:

# 1. Clase base: DispositivoElectrico
#    - Atributos: voltaje, consumo_watts, encendido
#    - Métodos: encender(), apagar(), calcular_consumo_diario()

# 2. Subclase: Motor (hereda de DispositivoElectrico)
#    - Atributos adicionales: rpm, torque
#    - Métodos adicionales: ajustar_velocidad(rpm)

# 3. Subclase: Bomba (hereda de Motor)
#    - Atributos adicionales: caudal_max
#    - Métodos adicionales: bombear(tiempo_segundos)

# Tu código acá

Ejercicio 3: Sistema de notificaciones con polimorfismo

Implementá un sistema que pueda enviar notificaciones por diferentes medios:

# 1. Clase abstracta: CanalNotificacion
#    - Método abstracto: enviar(mensaje)

# 2. Implementaciones concretas:
#    - NotificacionWhatsApp
#    - NotificacionTelegram
#    - NotificacionPantallaLED

# 3. Clase GestorNotificaciones que:
#    - Permita registrar múltiples canales
#    - Envíe mensajes por todos los canales registrados

# Tu código acá

🚀 Proyecto integrador: Sistema de monitoreo industrial

Diseñá un sistema completo que integre todo lo aprendido:

Requisitos:

  1. Sensores (herencia):

    • Clase base Sensor
    • Subclases: SensorTemperatura, SensorPresion, SensorNivel
  2. Actuadores (herencia):

    • Clase base Actuador
    • Subclases: Valvula, Bomba, Motor
  3. Sistema de control (composición):

    • Clase ControladorProceso que contenga:
      • Lista de sensores
      • Lista de actuadores
      • Sistema de alarmas
      • Base de datos (simulada)
  4. Funcionalidades:

    • Leer valores de sensores
    • Controlar actuadores basándose en lecturas
    • Generar alarmas cuando hay valores fuera de rango
    • Registrar todo en un log

Ejemplo de uso esperado:

# Crear el sistema
controlador = ControladorProceso("Planta Principal")

# Agregar dispositivos
controlador.agregar_sensor(SensorTemperatura("TEMP-001", "Caldera"))
controlador.agregar_sensor(SensorPresion("PRES-001", "Línea A"))
controlador.agregar_actuador(Valvula("VALV-001", "Entrada"))

# Simular operación
controlador.actualizar_sensor("TEMP-001", 85)
controlador.actualizar_sensor("PRES-001", 7.5)

# Control automático
if controlador.obtener_lectura("TEMP-001") > 80:
    controlador.activar_actuador("VALV-001", apertura=50)

# Reporte
controlador.generar_reporte()

📚 Comparación: POO vs otros paradigmas

AspectoEstructuradoFuncionalPOO
OrganizaciónFunciones y procedimientosFunciones purasObjetos y clases
EstadoVariables globales/localesInmutableEncapsulado en objetos
ReutilizaciónFuncionesComposición de funcionesHerencia y composición
Ideal paraScripts simplesProcesamiento de datosSistemas complejos
EjemploControl secuencialAnálisis de datosSistema SCADA

💡 Consejos y buenas prácticas

✅ Hacé esto:

  1. Nombres descriptivos: SensorTemperatura es mejor que ST
  2. Una responsabilidad por clase: Cada clase debe hacer una cosa bien
  3. Encapsulá datos sensibles: Usá atributos privados cuando sea necesario
  4. Documentá tu código: Usá docstrings
  5. Preferí composición sobre herencia: No abuses de la herencia
  6. Seguí el principio DRY: Don’t Repeat Yourself

❌ Evitá esto:

  1. Clases demasiado grandes: Si tiene más de 300 líneas, probablemente debas dividirla
  2. Herencia profunda: Más de 3-4 niveles se vuelve confuso
  3. Atributos públicos para todo: Protegé tus datos
  4. Métodos que hacen demasiado: Dividí en métodos más pequeños
  5. Nombres genéricos: data, info, temp no dicen nada

🎯 Conclusión

La Programación Orientada a Objetos es fundamental para tu carrera en Automatización y Control de Procesos Industriales. Te permite:

Recordá: POO no es solo sintaxis, es una forma de pensar. Practicá modelando objetos del mundo real, empezá simple y andá aumentando la complejidad.

📖 Próximos pasos:

  1. Resolvé los ejercicios propuestos
  2. Intentá el proyecto integrador
  3. Investigá más sobre patrones de diseño
  4. Aplicá POO en tus proyectos de SCADA y control

🔗 Recursos adicionales:


¿Tenés dudas? ¡Preguntá en clase o dejá un comentario! 💬

¡Ahora a programar con objetos! 🚀🏭