
Fundamentos de Programación: Programación Orientada a Objetos
Aprende los principios de la programación orientada a objetos con ejemplos prácticos en Python.
¡Domina la programación orientada a objetos! En este tutorial completo te guiaré paso a paso para que aprendas los principios fundamentales de la POO, desde clases básicas hasta patrones de diseño avanzados.
Objetivo: Aprender los cuatro pilares de la POO (encapsulación, herencia, polimorfismo y abstracción), implementar clases y objetos, y aplicar patrones de diseño básicos en Python.
Paso 1: ¿Qué es la programación orientada a objetos?
La POO es un paradigma de programación que organiza el código en objetos que representan entidades del mundo real. Es como construir con bloques LEGO: piezas independientes que se conectan para crear sistemas complejos.
# Sin POO - código procedural
def crear_perfil(nombre, edad, email):
return {"nombre": nombre, "edad": edad, "email": email}
def mostrar_perfil(perfil):
print(f"Nombre: {perfil['nombre']}")
print(f"Edad: {perfil['edad']}")
print(f"Email: {perfil['email']}")
# Con POO - código organizado en objetos
class Perfil:
def __init__(self, nombre, edad, email):
self.nombre = nombre
self.edad = edad
self.email = email
def mostrar_info(self):
print(f"Nombre: {self.nombre}")
print(f"Edad: {self.edad}")
print(f"Email: {self.email}")
Paso 2: Encapsulación - proteger los datos
La encapsulación permite proteger los datos internos de una clase y controlar el acceso a ellos.
class CuentaBancaria:
def __init__(self, titular, saldo_inicial=0):
self.titular = titular
self.__saldo = saldo_inicial # __ hace el atributo privado
# Métodos públicos para interactuar con los datos privados
def depositar(self, cantidad):
if cantidad > 0:
self.__saldo += cantidad
print(f"Depósito exitoso. Nuevo saldo: {self.__saldo}")
else:
print("La cantidad debe ser positiva")
def retirar(self, cantidad):
if 0 < cantidad <= self.__saldo:
self.__saldo -= cantidad
print(f"Retiro exitoso. Nuevo saldo: {self.__saldo}")
else:
print("Fondos insuficientes o cantidad inválida")
def consultar_saldo(self):
return self.__saldo
# Uso de la clase
mi_cuenta = CuentaBancaria("Ana", 1000)
mi_cuenta.depositar(500)
mi_cuenta.retirar(200)
print(f"Saldo actual: {mi_cuenta.consultar_saldo()}")
# mi_cuenta.__saldo # Error: atributo privado
Paso 3: Herencia - reutilizar y extender
La herencia permite crear nuevas clases basadas en clases existentes, heredando sus características y comportamientos.
# Clase base (padre)
class Animal:
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
def hacer_sonido(self):
print("El animal hace un sonido")
def dormir(self):
print(f"{self.nombre} está durmiendo")
# Clases derivadas (hijas)
class Perro(Animal): # Herencia de Animal
def __init__(self, nombre, edad, raza):
super().__init__(nombre, edad) # Llamar al constructor del padre
self.raza = raza
# Sobrescribir método
def hacer_sonido(self):
print("¡Guau! ¡Guau!")
# Método específico de Perro
def buscar_hueso(self):
print(f"{self.nombre} está buscando un hueso")
class Gato(Animal):
def __init__(self, nombre, edad, vidas=9):
super().__init__(nombre, edad)
self.vidas = vidas
def hacer_sonido(self):
print("¡Miau! ¡Miau!")
def usar_vida(self):
if self.vidas > 0:
self.vidas -= 1
print(f"{self.nombre} perdió una vida. Vidas restantes: {self.vidas}")
else:
print(f"{self.nombre} no tiene vidas restantes")
# Usamos las clases
mi_perro = Perro("Max", 3, "Labrador")
mi_gato = Gato("Luna", 2)
mi_perro.hacer_sonido() # ¡Guau! ¡Guau!
mi_gato.hacer_sonido() # ¡Miau! ¡Miau!
mi_perro.dormir() # Heredado de Animal
mi_gato.usar_vida() # Específico de Gato
Paso 4: Polimorfismo - múltiples formas
El polimorfismo permite que diferentes objetos respondan al mismo método de diferente manera.
# Polimorfismo: diferentes objetos responden al mismo método de diferente forma
class Pajaro(Animal):
def hacer_sonido(self):
print("¡Pío! ¡Pío!")
class Vaca(Animal):
def hacer_sonido(self):
print("¡Muuu!")
# Lista de animales diferentes
animales = [
Perro("Rex", 2, "Pastor Alemán"),
Gato("Whiskers", 4),
Pajaro("Piolín", 1),
Vaca("Lola", 5)
]
# Todos responden al mismo método de diferente forma
for animal in animales:
print(f"{animal.nombre}: ", end="")
animal.hacer_sonido()
Paso 5: Abstracción - ocultar complejidad
La abstracción permite definir interfaces comunes sin especificar la implementación completa.
from abc import ABC, abstractmethod
# Clase abstracta - define interface pero no implementación completa
class Forma(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimetro(self):
pass
# Método concreto (implementado)
def describir(self):
print(f"Soy una forma geométrica")
# Clases concretas que implementan la abstracción
class Rectangulo(Forma):
def __init__(self, ancho, alto):
self.ancho = ancho
self.alto = alto
def area(self):
return self.ancho * self.alto
def perimetro(self):
return 2 * (self.ancho + self.alto)
class Circulo(Forma):
def __init__(self, radio):
self.radio = radio
def area(self):
return 3.1416 * self.radio ** 2
def perimetro(self):
return 2 * 3.1416 * self.radio
# No podemos instanciar Forma directamente
# forma = Forma() # Error!
# Pero sí sus implementaciones concretas
rect = Rectangulo(5, 3)
circ = Circulo(4)
print(f"Área del rectángulo: {rect.area()}")
print(f"Perímetro del círculo: {circ.perimetro()}")
Paso 6: Métodos especiales (dunder methods)
Los métodos especiales permiten sobrecargar operadores y personalizar el comportamiento de las clases.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
# __str__ para representación legible
def __str__(self):
return f"Vector({self.x}, {self.y})"
# __repr__ para representación técnica
def __repr__(self):
return f"Vector({self.x}, {self.y})"
# __add__ para sobrecarga del operador +
def __add__(self, otro):
return Vector(self.x + otro.x, self.y + otro.y)
# __mul__ para sobrecarga del operador *
def __mul__(self, escalar):
return Vector(self.x * escalar, self.y * escalar)
# __len__ para la función len()
def __len__(self):
return int((self.x**2 + self.y**2)**0.5)
# __getitem__ para acceso con []
def __getitem__(self, index):
if index == 0:
return self.x
elif index == 1:
return self.y
else:
raise IndexError("Índice fuera de rango")
# Usamos los métodos especiales
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1) # Vector(3, 4) - usa __str__
print(v1 + v2) # Vector(4, 6) - usa __add__
print(v1 * 2) # Vector(6, 8) - usa __mul__
print(len(v1)) # 5 - usa __len__
print(v1[0]) # 3 - usa __getitem__
Paso 7: Propiedades y decoradores
Las propiedades permiten controlar el acceso a los atributos de una clase.
class Persona:
def __init__(self, nombre, edad):
self._nombre = nombre
self._edad = edad
# Property para acceso controlado
@property
def nombre(self):
return self._nombre.title() # Siempre con mayúscula inicial
@nombre.setter
def nombre(self, valor):
if isinstance(valor, str) and valor.strip():
self._nombre = valor.strip()
else:
raise ValueError("Nombre debe ser un string no vacío")
@property
def edad(self):
return self._edad
@edad.setter
def edad(self, valor):
if 0 <= valor <= 120:
self._edad = valor
else:
raise ValueError("Edad debe estar entre 0 y 120")
# Método de clase
@classmethod
def desde_nacimiento(cls, nombre, año_nacimiento):
from datetime import datetime
año_actual = datetime.now().year
edad = año_actual - año_nacimiento
return cls(nombre, edad)
# Método estático
@staticmethod
def es_mayor_de_edad(edad):
return edad >= 18
# Usamos properties
persona = Persona("ana", 25)
print(persona.nombre) # Ana (con mayúscula)
persona.nombre = " carlos "
print(persona.nombre) # Carlos (sin espacios)
# Usamos classmethod
persona2 = Persona.desde_nacimiento("Laura", 1995)
print(f"{persona2.nombre} tiene {persona2.edad} años")
# Usamos staticmethod
print(f"¿20 años es mayor de edad? {Persona.es_mayor_de_edad(20)}")
Paso 8: Proyecto completo - sistema de biblioteca
Vamos a crear un sistema completo de gestión de biblioteca usando POO.
class Libro:
def __init__(self, titulo, autor, isbn, disponible=True):
self.titulo = titulo
self.autor = autor
self.isbn = isbn
self.disponible = disponible
def __str__(self):
estado = "Disponible" if self.disponible else "Prestado"
return f"'{self.titulo}' por {self.autor} - {estado}"
class Usuario:
def __init__(self, nombre, id_usuario):
self.nombre = nombre
self.id_usuario = id_usuario
self.libros_prestados = []
def tomar_prestado(self, libro):
if libro.disponible:
libro.disponible = False
self.libros_prestados.append(libro)
print(f"{self.nombre} tomó prestado '{libro.titulo}'")
else:
print(f"'{libro.titulo}' no está disponible")
def devolver(self, libro):
if libro in self.libros_prestados:
libro.disponible = True
self.libros_prestados.remove(libro)
print(f"{self.nombre} devolvió '{libro.titulo}'")
else:
print(f"{self.nombre} no tiene prestado '{libro.titulo}'")
class Biblioteca:
def __init__(self):
self.libros = []
self.usuarios = []
def agregar_libro(self, libro):
self.libros.append(libro)
print(f"Libro agregado: {libro}")
def registrar_usuario(self, usuario):
self.usuarios.append(usuario)
print(f"Usuario registrado: {usuario.nombre}")
def buscar_libro(self, titulo):
for libro in self.libros:
if titulo.lower() in libro.titulo.lower():
yield libro
def mostrar_estado(self):
print("\n--- Estado de la Biblioteca ---")
print(f"Libros: {len(self.libros)}")
print(f"Usuarios: {len(self.usuarios)}")
disponibles = sum(1 for libro in self.libros if libro.disponible)
print(f"Libros disponibles: {disponibles}")
print(f"Libros prestados: {len(self.libros) - disponibles}")
# Usamos el sistema de biblioteca
biblioteca = Biblioteca()
# Agregamos libros
biblioteca.agregar_libro(Libro("Cien años de soledad", "Gabriel García Márquez", "12345"))
biblioteca.agregar_libro(Libro("1984", "George Orwell", "67890"))
biblioteca.agregar_libro(Libro("El principito", "Antoine de Saint-Exupéry", "13579"))
# Registramos usuarios
usuario1 = Usuario("Ana", "001")
usuario2 = Usuario("Carlos", "002")
biblioteca.registrar_usuario(usuario1)
biblioteca.registrar_usuario(usuario2)
# Realizamos operaciones
usuario1.tomar_prestado(biblioteca.libros[0])
usuario2.tomar_prestado(biblioteca.libros[1])
biblioteca.mostrar_estado()
# Buscamos libros
print("\nBuscando 'soledad':")
for libro in biblioteca.buscar_libro("soledad"):
print(f"Encontrado: {libro}")
Paso 9: Patrones de diseño básicos
Singleton - una única instancia
class Configuracion:
_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.tema = "oscuro"
self.idioma = "español"
self.modo_debug = False
def __str__(self):
return f"Configuración: {self.tema}, {self.idioma}, Debug: {self.modo_debug}"
# Siempre obtenemos la misma instancia
config1 = Configuracion()
config2 = Configuracion()
print(config1 is config2) # True - misma instancia
print(config1)
Factory - creación flexible de objetos
class FabricaAnimales:
@staticmethod
def crear_animal(tipo, *args, **kwargs):
if tipo == "perro":
return Perro(*args, **kwargs)
elif tipo == "gato":
return Gato(*args, **kwargs)
elif tipo == "pajaro":
return Pajaro(*args, **kwargs)
else:
raise ValueError(f"Tipo de animal desconocido: {tipo}")
# Usamos la fábrica
animal1 = FabricaAnimales.crear_animal("perro", "Rex", 3, "Labrador")
animal2 = FabricaAnimales.crear_animal("gato", "Luna", 2)
animal1.hacer_sonido()
animal2.hacer_sonido()
Paso 10: Buenas prácticas en POO
Principio de responsabilidad única
# ❌ Mal: Una clase con múltiples responsabilidades
class EmpleadoMalDiseñado:
def __init__(self, nombre, salario):
self.nombre = nombre
self.salario = salario
def calcular_pago(self):
# Lógica de negocio
return self.salario * 0.9 # Descuento de impuestos
def guardar_en_bd(self):
# Lógica de persistencia
print("Guardando en base de datos...")
def enviar_email(self):
# Lógica de notificación
print("Enviando email...")
# ✅ Bien: Clases con responsabilidad única
class Empleado:
def __init__(self, nombre, salario):
self.nombre = nombre
self.salario = salario
def calcular_pago(self):
return self.salario * 0.9
class RepositorioEmpleado:
def guardar(self, empleado):
print(f"Guardando {empleado.nombre} en BD")
class ServicioEmail:
def enviar_notificacion(self, empleado):
print(f"Enviando email a {empleado.nombre}")
Composición sobre herencia
# En lugar de heredar todo, componemos con partes
class Motor:
def encender(self):
print("Motor encendido")
def apagar(self):
print("Motor apagado")
class Ruedas:
def __init__(self, cantidad):
self.cantidad = cantidad
def girar(self):
print(f"{self.cantidad} ruedas girando")
class Coche:
def __init__(self, marca, modelo):
self.marca = marca
self.modelo = modelo
self.motor = Motor() # Composición
self.ruedas = Ruedas(4) # Composición
def conducir(self):
self.motor.encender()
self.ruedas.girar()
print(f"Conduciendo {self.marca} {self.modelo}")
mi_coche = Coche("Toyota", "Corolla")
mi_coche.conducir()
Paso 11: Ejercicios de práctica
# Ejercicio 1: Sistema de figuras geométricas
class FiguraGeometrica:
def area(self):
raise NotImplementedError("Método area() no implementado")
def perimetro(self):
raise NotImplementedError("Método perimetro() no implementado")
class Cuadrado(FiguraGeometrica):
def __init__(self, lado):
self.lado = lado
def area(self):
return self.lado ** 2
def perimetro(self):
return 4 * self.lado
class Triangulo(FiguraGeometrica):
def __init__(self, base, altura, lado1, lado2, lado3):
self.base = base
self.altura = altura
self.lado1 = lado1
self.lado2 = lado2
self.lado3 = lado3
def area(self):
return (self.base * self.altura) / 2
def perimetro(self):
return self.lado1 + self.lado2 + self.lado3
# Ejercicio 2: Sistema de notificaciones
class Notificador:
def enviar(self, mensaje):
raise NotImplementedError("Método enviar() no implementado")
class EmailNotificador(Notificador):
def enviar(self, mensaje):
print(f"Enviando email: {mensaje}")
class SMSNotificador(Notificador):
def enviar(self, mensaje):
print(f"Enviando SMS: {mensaje}")
class PushNotificador(Notificador):
def enviar(self, mensaje):
print(f"Enviando notificación push: {mensaje}")
class ServicioNotificaciones:
def __init__(self):
self.notificadores = []
def agregar_notificador(self, notificador):
self.notificadores.append(notificador)
def enviar_todos(self, mensaje):
for notificador in self.notificadores:
notificador.enviar(mensaje)
# Probamos el sistema de notificaciones
servicio = ServicioNotificaciones()
servicio.agregar_notificador(EmailNotificador())
servicio.agregar_notificador(SMSNotificador())
servicio.agregar_notificador(PushNotificador())
servicio.enviar_todos("¡Hola! Este es un mensaje importante")
Paso 12: Próximos pasos en POO
Temas para profundizar:
- Patrones de diseño avanzados: Observer, Strategy, Command
- POO en frameworks: Django, Flask, FastAPI
- Testing con POO: Unit tests, mock objects
- Arquitectura de software: Clean Architecture, Hexagonal Architecture
Paso 13: Recursos y herramientas
Recursos para aprender más:
- Libros: "Head First Design Patterns", "Clean Code" de Robert C. Martin
- Plataformas: Real Python, Python Tricks
- Comunidades: Stack Overflow, Reddit r/learnpython
Proyectos para implementar:
- Sistema de reservas de hotel
- Juego de ajedrez o damas
- Simulador de ecosistema
- Gestor de tareas avanzado
Conclusión
¡Felicidades! Has dominado los fundamentos de la programación orientada a objetos. Practica estos conceptos creando proyectos reales y aplicando los patrones de diseño.
Para más tutoriales sobre POO y patrones de diseño, visita nuestra sección de tutoriales.
¡Con estos conocimientos ya puedes crear aplicaciones orientadas a objetos!
💡 Tip Importante
📝 Mejores Prácticas en Programación Orientada a Objetos
Para escribir código POO de calidad, considera estos consejos esenciales:
- Sigue el principio de responsabilidad única: Cada clase debe tener una sola razón para cambiar
- Usa composición sobre herencia: Prefiere composición cuando sea posible para mayor flexibilidad
- Programa para interfaces, no implementaciones: Usa clases abstractas para definir contratos
- Mantén la encapsulación: Protege los datos internos y expone solo lo necesario
- Nombra claramente: Usa nombres descriptivos para clases, métodos y atributos
- Documenta tu código: Usa docstrings para explicar el propósito de clases y métodos
- Escribe tests: Crea pruebas unitarias para validar el comportamiento de tus clases
- Refactoriza regularmente: Mejora el diseño de tu código a medida que evoluciona
📚 Documentación: Revisa la documentación oficial de Python sobre clases y objetos aquí y patrones de diseño aquí
¡Estos consejos te ayudarán a escribir código POO mantenible y escalable!
No hay comentarios aún
Sé el primero en comentar este tutorial.