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:
- Estado (atributos/propiedades): lo que el objeto “sabe”
- Comportamiento (métodos): lo que el objeto “puede hacer”
- Identidad: es único, aunque tenga los mismos valores que otro objeto
¿Qué es una clase?
Una clase es el molde, la plantilla, el plano arquitectónico para crear objetos. Define:
- Qué atributos tendrán los objetos
- Qué métodos podrán ejecutar
- Cómo se inicializarán
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:
- Público (
atributo): Accesible desde cualquier lugar - Protegido (
_atributo): Por convención, solo para uso interno - Privado (
__atributo): Python lo “oculta” mediante name mangling
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?
- ✅ Protege datos críticos
- ✅ Permite validar datos antes de modificarlos
- ✅ Facilita el mantenimiento (podés cambiar la implementación interna sin afectar el código externo)
- ✅ Previene errores por modificaciones accidentales
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:
- ✅ Reutilización de código
- ✅ Jerarquías lógicas y organizadas
- ✅ Facilita el mantenimiento
- ✅ Permite especialización gradual
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:
-
Sensores (herencia):
- Clase base
Sensor - Subclases:
SensorTemperatura,SensorPresion,SensorNivel
- Clase base
-
Actuadores (herencia):
- Clase base
Actuador - Subclases:
Valvula,Bomba,Motor
- Clase base
-
Sistema de control (composición):
- Clase
ControladorProcesoque contenga:- Lista de sensores
- Lista de actuadores
- Sistema de alarmas
- Base de datos (simulada)
- Clase
-
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
| Aspecto | Estructurado | Funcional | POO |
|---|---|---|---|
| Organización | Funciones y procedimientos | Funciones puras | Objetos y clases |
| Estado | Variables globales/locales | Inmutable | Encapsulado en objetos |
| Reutilización | Funciones | Composición de funciones | Herencia y composición |
| Ideal para | Scripts simples | Procesamiento de datos | Sistemas complejos |
| Ejemplo | Control secuencial | Análisis de datos | Sistema SCADA |
💡 Consejos y buenas prácticas
✅ Hacé esto:
- Nombres descriptivos:
SensorTemperaturaes mejor queST - Una responsabilidad por clase: Cada clase debe hacer una cosa bien
- Encapsulá datos sensibles: Usá atributos privados cuando sea necesario
- Documentá tu código: Usá docstrings
- Preferí composición sobre herencia: No abuses de la herencia
- Seguí el principio DRY: Don’t Repeat Yourself
❌ Evitá esto:
- Clases demasiado grandes: Si tiene más de 300 líneas, probablemente debas dividirla
- Herencia profunda: Más de 3-4 niveles se vuelve confuso
- Atributos públicos para todo: Protegé tus datos
- Métodos que hacen demasiado: Dividí en métodos más pequeños
- Nombres genéricos:
data,info,tempno 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:
- ✅ Modelar sistemas industriales complejos de manera intuitiva
- ✅ Crear código reutilizable y mantenible
- ✅ Trabajar en equipo de manera más efectiva
- ✅ Diseñar sistemas SCADA, HMI y controladores robustos
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:
- Resolvé los ejercicios propuestos
- Intentá el proyecto integrador
- Investigá más sobre patrones de diseño
- Aplicá POO en tus proyectos de SCADA y control
🔗 Recursos adicionales:
- 📓 Google Colab con la Tarea
- Documentación oficial de Python sobre clases
- Libro: “Design Patterns” (Gang of Four)
- Practicá en proyectos reales de automatización
¿Tenés dudas? ¡Preguntá en clase o dejá un comentario! 💬
¡Ahora a programar con objetos! 🚀🏭