Introducción
Cuando un programa crece, tener todo el código en una lista de funciones sueltas se vuelve inmanejable. La programación orientada a objetos (POO) resuelve ese problema organizando el código a imagen del mundo real: en lugar de pensar en «funciones y datos», piensas en «cosas» (un alumno, una cuenta, un coche) que guardan su propia información y saben hacer determinadas acciones.
En este artículo aprenderás los cuatro conceptos centrales de la POO: clases y objetos, encapsulamiento, herencia y polimorfismo, con ejemplos en Python (cuyo código está ejecutado y las salidas son reales). Las ideas son las mismas en Java, C++, C# y otros lenguajes; solo cambia la sintaxis.
1. Clases y objetos
Una clase es el molde o plano de un tipo de cosa: define qué datos tiene (atributos) y qué puede hacer (métodos). Un objeto (o instancia) es una pieza concreta fabricada con ese molde. De una clase Perro se pueden crear tantos perros como se quiera, cada uno con su nombre y su edad.
class Perro:
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
def ladrar(self):
return f"{self.nombre} dice: ¡Guau!"
def cumplir_anios(self):
self.edad += 1
toby = Perro("Toby", 3)
print(toby.ladrar())
toby.cumplir_anios()
print(toby.edad)El programa imprime:
Toby dice: ¡Guau! 4
__init__es el constructor: se ejecuta al crear el objeto y le da sus valores iniciales.selfes el propio objeto:self.nombrees «el nombre de este perro».toby.ladrar()es una llamada a un método;toby.edadaccede a un atributo.
Perro es una clase; toby es un objeto. Cambiar la edad de toby no afecta a otros perros.2. Encapsulamiento
El encapsulamiento consiste en proteger el estado interno de un objeto y obligar a usarlo a través de sus métodos. Así el objeto puede garantizar que sus datos son siempre válidos: una cuenta bancaria no debería permitir que alguien ponga el saldo en un valor negativo escribiendo directamente cuenta.saldo = -1000.
class CuentaBancaria:
def __init__(self, titular, saldo_inicial=0):
self.titular = titular
self._saldo = saldo_inicial # el guion bajo indica "uso interno"
@property
def saldo(self):
return self._saldo
def depositar(self, cantidad):
if cantidad <= 0:
raise ValueError("La cantidad debe ser positiva")
self._saldo += cantidad
def retirar(self, cantidad):
if cantidad > self._saldo:
raise ValueError("Saldo insuficiente")
self._saldo -= cantidad
cuenta = CuentaBancaria("Ana", 100)
cuenta.depositar(50)
cuenta.retirar(30)
print(cuenta.saldo)
try:
cuenta.retirar(500)
except ValueError as error:
print("Error:", error)Salida:
120 Error: Saldo insuficiente
El saldo solo cambia a través de depositar y retirar, que comprueban las reglas antes de modificarlo. En Python la convención es empezar por un guion bajo los atributos «internos» (_saldo); en Java o C++ existen modificadores como private que lo impiden de verdad.
3. Herencia
La herencia permite crear una clase nueva a partir de otra, reutilizando todo lo que ya tiene. La clase base (superclase) recoge lo común y las derivadas (subclases) añaden o cambian lo particular. Se usa cuando se cumple la relación «es un»: un perro es un animal.
Diagrama UML: Perro y Gato heredan de Animal.
4. Polimorfismo
Polimorfismo significa «muchas formas»: objetos distintos responden de forma distinta al mismo mensaje. En el ejemplo, todos los animales tienen un método hablar(), pero cada subclase lo redefine (sobrescritura) a su manera:
class Animal:
def __init__(self, nombre):
self.nombre = nombre
def hablar(self):
return "..."
def presentarse(self):
return f"Soy {self.nombre} y hago: {self.hablar()}"
class Perro(Animal):
def __init__(self, nombre, raza):
super().__init__(nombre) # reutiliza el constructor de Animal
self.raza = raza
def hablar(self): # redefine el método heredado
return "¡Guau!"
class Gato(Animal):
def hablar(self):
return "¡Miau!"
animales = [Perro("Toby", "labrador"), Gato("Misi"), Animal("Bicho")]
for animal in animales:
print(animal.presentarse())Salida:
Soy Toby y hago: ¡Guau! Soy Misi y hago: ¡Miau! Soy Bicho y hago: ...
El bucle for no necesita saber de qué tipo es cada animal: llama a presentarse() y cada objeto hace lo que le corresponde. Si mañana se añade la clase Vaca, el bucle sigue funcionando sin tocarlo. Esa es la gran ventaja: código flexible y fácil de ampliar.
Fíjate también en super().__init__(nombre): llama al constructor de la clase base para no repetir su código.
5. Resumen de los conceptos
6. Ejemplo resuelto: figuras geométricas
Enunciado. Diseña un programa que calcule el área total de una lista de figuras formada por círculos y rectángulos, de forma que se puedan añadir más tipos de figura sin cambiar el cálculo del total.
Solución. La clase base Figura declara que toda figura debe tener un método area() (si una subclase no lo implementa, se lanza un error). Cada figura concreta lo implementa con su fórmula: para el círculo y para el rectángulo.
import math
class Figura:
def area(self):
raise NotImplementedError("Cada figura debe calcular su área")
class Circulo(Figura):
def __init__(self, radio):
self.radio = radio
def area(self):
return math.pi * self.radio ** 2
class Rectangulo(Figura):
def __init__(self, base, altura):
self.base = base
self.altura = altura
def area(self):
return self.base * self.altura
figuras = [Circulo(2), Rectangulo(3, 4), Circulo(1)]
for figura in figuras:
print(type(figura).__name__, round(figura.area(), 2))
total = sum(figura.area() for figura in figuras)
print("Área total:", round(total, 2))Salida:
Circulo 12.57 Rectangulo 12 Circulo 3.14 Área total: 27.71
Comprobación a mano: , y ; la suma es . Para añadir un triángulo bastaría con crear la clase Triangulo con su area(): el bucle y la suma no cambian. Esto es polimorfismo en acción.
7. Herencia o composición
No todo se resuelve con herencia. Cuando la relación es «tiene un» en lugar de «es un», se usa composición: un objeto contiene a otro como atributo. Un coche tiene un motor, pero no es un motor.
class Motor:
def __init__(self, potencia):
self.potencia = potencia
class Coche:
def __init__(self, modelo, potencia):
self.modelo = modelo
self.motor = Motor(potencia) # un coche TIENE un motor
coche = Coche("Compacto", 110)
print(coche.modelo, coche.motor.potencia, "CV")Salida:
Compacto 110 CV
8. Errores frecuentes
- ⚠️ Confundir clase y objeto. La clase es la definición; el objeto es lo que se crea al ejecutar
Perro("Toby", 3) - ⚠️ Olvidar
self. Dentro de un método hay que escribirself.nombre; si pones solonombre, Python busca una variable local y falla - ⚠️ Usar herencia cuando no hay relación «es un». Un coche no hereda de motor. Si solo quieres reutilizar código, suele ser mejor la composición
- ⚠️ Modificar atributos «internos» desde fuera. Se rompe el encapsulamiento y se pueden dejar datos inválidos; usa los métodos que la clase ofrece
- ⚠️ Olvidar llamar a
super().__init__(). Si la subclase tiene constructor propio y no llama al de la clase base, los atributos de la base no se crean
9. 📌 En 30 segundos
- Una clase es el molde (atributos y métodos); un objeto es una instancia concreta.
- Encapsulamiento: el estado se protege y solo se cambia mediante métodos que validan.
- Herencia: una subclase reutiliza y amplía una superclase (relación «es un»);
super()accede a la base. - Polimorfismo: objetos distintos responden de forma distinta al mismo método.
- Para relaciones «tiene un» se usa composición: un objeto guarda a otro como atributo.