Imagina que tienes que representar en código un videojuego con personajes, vehículos en una app de flotas, o simplemente usuarios y pedidos en una tienda online. En algún momento vas a tener decenas de variables sueltas (nombre, vida, velocidad, email, precio) y funciones que las manipulan por todas partes, sin ningún orden. La programación orientada a objetos (POO) nace precisamente para resolver ese caos: agrupar datos y el comportamiento que los manipula en una sola unidad.
Es el paradigma que sostiene la mayoría del código empresarial que vas a encontrar en tu carrera, desde componentes de React hasta modelos de base de datos en un backend. No hace falta dominarlo entero de golpe; con cuatro conceptos bien entendidos ya puedes leer y escribir código orientado a objetos con soltura.
La unidad básica: clases y objetos
Una clase es un molde: define qué datos (atributos) y qué comportamiento (métodos) va a tener cada cosa que crees a partir de ella. Un objeto es una instancia concreta de esa clase, con valores propios. Si Coche es la clase, "el Seat rojo del garaje de mi vecino" es un objeto de esa clase.
class Coche:
def __init__(self, marca, modelo, velocidad_max):
self.marca = marca
self.modelo = modelo
self.velocidad_max = velocidad_max
self.velocidad_actual = 0
def acelerar(self, incremento):
self.velocidad_actual = min(
self.velocidad_actual + incremento,
self.velocidad_max
)
mi_coche = Coche("Seat", "Ibiza", 180)
mi_coche.acelerar(40)
print(mi_coche.velocidad_actual) # 40
Cada vez que escribes Coche("Seat", "Ibiza", 180) se ejecuta el método especial __init__ (el "constructor") y se crea un objeto nuevo, independiente de cualquier otro coche que hayas creado antes. Esa independencia es la base de todo lo demás.
Cuatro pilares, explicados sin rodeos
La teoría clásica de POO se resume en cuatro pilares. En vez de definirlos con frases abstractas, aquí tienes cada uno con el problema concreto que resuelve y su código correspondiente.
Ya lo has visto arriba: la clase define la estructura, el objeto es cada instancia concreta con sus propios valores. Puedes crear tantos coches como quieras a partir de la misma clase Coche, y cada uno mantiene su propia velocidad_actual sin afectar a los demás. Este aislamiento es lo que hace manejable modelar sistemas grandes: cada objeto se ocupa de sus propios datos.
coche_a = Coche("Toyota", "Corolla", 190)
coche_b = Coche("Renault", "Clio", 170)
coche_a.acelerar(60)
print(coche_a.velocidad_actual) # 60
print(coche_b.velocidad_actual) # 0, no se ve afectado
Cuando dos clases comparten mucho comportamiento pero tienen diferencias concretas, la herencia te permite definir una clase "base" con lo común y clases "hijas" que añaden o modifican lo específico, sin copiar y pegar código.
class CocheElectrico(Coche):
def __init__(self, marca, modelo, velocidad_max, autonomia_km):
super().__init__(marca, modelo, velocidad_max)
self.autonomia_km = autonomia_km
def cargar_bateria(self):
print(f"{self.marca} {self.modelo} cargando...")
tesla = CocheElectrico("Tesla", "Model 3", 225, 500)
tesla.acelerar(80) # heredado de Coche
tesla.cargar_bateria() # propio de CocheElectrico
CocheElectrico hereda todo lo de Coche (el atributo marca, el método acelerar) y añade lo suyo propio. super().__init__(...) llama al constructor de la clase padre para no repetir esa lógica.
Encapsular significa que el estado interno de un objeto solo se modifica a través de sus propios métodos, no accediendo y cambiando los atributos directamente desde fuera. Esto evita que otra parte del programa deje al objeto en un estado inconsistente, como una velocidad negativa o un saldo bancario alterado sin pasar por las validaciones correspondientes.
class CuentaBancaria:
def __init__(self, saldo_inicial):
self.__saldo = saldo_inicial # atributo "privado"
def retirar(self, cantidad):
if cantidad > self.__saldo:
raise ValueError("Saldo insuficiente")
self.__saldo -= cantidad
def consultar_saldo(self):
return self.__saldo
cuenta = CuentaBancaria(100)
cuenta.retirar(30)
print(cuenta.consultar_saldo()) # 70
# cuenta.__saldo = -500 # esto no funciona directamente, y es la idea
El doble guion bajo (__saldo) le dice a Python que trate el atributo como privado. Cualquier cambio de saldo tiene que pasar por retirar, que valida que la operación tenga sentido antes de aplicarla.
Polimorfismo significa que distintas clases pueden implementar un método con el mismo nombre, pero cada una con su propia lógica. El código que llama al método no necesita saber de qué clase exacta es el objeto: solo necesita saber que "sabe" responder a ese método.
class Perro:
def hacer_sonido(self):
return "Guau"
class Gato:
def hacer_sonido(self):
return "Miau"
animales = [Perro(), Gato(), Perro()]
for animal in animales:
print(animal.hacer_sonido())
# Guau
# Miau
# Guau
El bucle no sabe ni le importa si cada elemento es un Perro o un Gato: simplemente llama a hacer_sonido() y cada objeto responde a su manera. Esto es lo que permite escribir código genérico que funciona con muchos tipos distintos de objetos sin necesidad de un if por cada caso.
¿Hace falta POO para programar bien?
No es obligatorio ni la única forma correcta de estructurar un programa: la programación funcional, por ejemplo, resuelve muchos de los mismos problemas de otra manera. Pero en la práctica vas a encontrar clases y objetos en casi cualquier lenguaje moderno y en casi cualquier framework, así que entender estos cuatro pilares no es opcional si quieres leer código ajeno con soltura, algo que se nota mucho en una entrevista técnica.
Un consejo práctico: no fuerces la POO donde no la necesitas. Si una función suelta resuelve el problema con claridad, no la envuelvas en una clase solo porque "así se hace". La POO brilla cuando modelas entidades con estado propio y comportamiento asociado; para scripts simples o transformaciones de datos, unas cuantas funciones bien organizadas suelen ser más legibles, algo que también se aplica al clean code en general.