← Todos los artículos
💻 Informática

Programación orientada a objetos: clases, objetos, herencia y polimorfismo

Aprende los cuatro pilares de la programación orientada a objetos (clases y objetos, encapsulamiento, herencia y polimorfismo) con código en Python ejecutado, diagramas UML y un ejercicio resuelto de figuras geométricas.

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.
  • self es el propio objeto: self.nombre es «el nombre de este perro».
  • toby.ladrar() es una llamada a un método; toby.edad accede a un atributo.
La clase es el molde y el objeto es la pieza. 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 de clases UML con la clase Animal arriba y las clases Perro y Gato debajo, unidas a Animal con flechas de herencia.

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

ConceptoQué significaEjemplo del artículo
ClaseMolde que define atributos y métodosPerro, CuentaBancaria
ObjetoInstancia concreta de una clasetoby, cuenta
EncapsulamientoProteger el estado y accederlo por métodos_saldo con depositar() y retirar()
HerenciaUna clase reutiliza y amplía otra («es un»)Perro hereda de Animal
PolimorfismoMismo mensaje, comportamiento distintohablar() en Perro, Gato y Animal

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: πr2\pi r^2 para el círculo y b⋅hb \cdot h 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: π⋅22≈12,57\pi \cdot 2^2 \approx 12{,}57, 3⋅4=123 \cdot 4 = 12 y π⋅12≈3,14\pi \cdot 1^2 \approx 3{,}14; la suma es 12,57+12+3,14≈27,7112{,}57 + 12 + 3{,}14 \approx 27{,}71. 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
Pregúntate: ¿X es un Y? Si la respuesta es sí, herencia. Si es «X tiene un Y», composición. La composición suele ser más flexible, y muchos programadores la prefieren cuando dudan.

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 escribir self.nombre; si pones solo nombre, 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

  1. Una clase es el molde (atributos y métodos); un objeto es una instancia concreta.
  2. Encapsulamiento: el estado se protege y solo se cambia mediante métodos que validan.
  3. Herencia: una subclase reutiliza y amplía una superclase (relación «es un»); super() accede a la base.
  4. Polimorfismo: objetos distintos responden de forma distinta al mismo método.
  5. Para relaciones «tiene un» se usa composición: un objeto guarda a otro como atributo.

Para seguir estudiando

¿Cuánto has aprendido?

1. ¿Qué es un objeto en POO?

2. ¿Para qué sirve el constructor __init__?

3. ¿Qué consigue el encapsulamiento?

4. Perro y Gato heredan de Animal. ¿Qué relación expresa esto?

5. ¿Qué es el polimorfismo?

6. Un coche contiene un motor. ¿Qué relación se debe usar entre sus clases?

7. En la clase Rectangulo(3, 4) del ejemplo, ¿cuánto devuelve area()?

¿Este tema sigue sin quedarte claro?

Si necesitas una explicación personalizada, puedes contactar con María.

Contactar con María