POO en Python: clases, herencia y composición

Actualizado el: 20/08/2026
Tempo de leitura: 4 minutos
logo do python com objetos abaixo do logo

La programación orientada a objetos en Python organiza un programa alrededor de objetos que combinan datos y comportamiento. Una cuenta bancaria puede almacenar un saldo y ofrecer métodos para depositar o retirar. Un producto puede guardar nombre y precio y calcular descuentos. Un personaje de videojuego puede conservar puntos de vida y ejecutar acciones.

Python permite utilizar orientación a objetos sin obligar a que todos los problemas se resuelvan con clases. En esta guía aprenderás clases, instancias, atributos, métodos, __init__, propiedades, métodos de clase, herencia, polimorfismo, composición y dataclasses.

Qué es un objeto

Un objeto es un valor que posee estado y comportamiento. Python ya trabaja con objetos:

nombre = "Python"
numeros = [3, 1, 2]

print(nombre.upper())
numeros.sort()
print(numeros)

El string proporciona upper() y la lista proporciona sort(). La documentación oficial sobre clases explica instancias, métodos e herencia.

Clase e instancia

Una clase define la estructura de un tipo. Una instancia es un objeto concreto creado a partir de esa clase:

class Producto:
    pass


libro = Producto()
curso = Producto()

print(type(libro))
print(libro is curso)

libro y curso son objetos distintos, aunque pertenecen a la misma clase.

Inicializar con __init__

class Producto:
    def __init__(self, nombre: str, precio: float) -> None:
        self.nombre = nombre
        self.precio = precio


libro = Producto("Manual de Python", 39.90)
print(libro.nombre)
print(libro.precio)

self representa la instancia actual. Al ejecutar Producto(...), Python crea el objeto y lo entrega al método __init__.

Atributos de instancia

self.nombre y self.precio pertenecen a cada objeto:

libro = Producto("Manual de Python", 39.90)
curso = Producto("Python Básico", 89.00)

print(libro.nombre)
print(curso.nombre)

Una variable local desaparece al terminar un método. Un atributo permanece unido a la instancia.

Métodos de instancia

class Producto:
    def __init__(self, nombre: str, precio: float) -> None:
        self.nombre = nombre
        self.precio = precio

    def aplicar_descuento(self, porcentaje: float) -> float:
        if not 0 <= porcentaje <= 100:
            raise ValueError("Porcentaje fuera de rango")
        return self.precio * (1 - porcentaje / 100)


libro = Producto("Manual de Python", 40)
print(libro.aplicar_descuento(15))

La validación evita resultados inválidos. La guía de excepciones en Python explica cómo diseñar errores útiles.

Representar objetos con __repr__

class Producto:
    def __init__(self, nombre: str, precio: float) -> None:
        self.nombre = nombre
        self.precio = precio

    def __repr__(self) -> str:
        return (
            f"Producto(nombre={self.nombre!r}, "
            f"precio={self.precio!r})"
        )

Una representación clara ayuda al depurar y registrar objetos. La guía de f-strings para depuración explica !r.

Atributos de clase

class Producto:
    moneda = "EUR"

    def __init__(self, nombre: str, precio: float) -> None:
        self.nombre = nombre
        self.precio = precio

Un atributo de clase representa una constante o valor compartido. Evita colocar listas o diccionarios mutables allí salvo que realmente quieras compartirlos.

El problema del atributo mutable compartido

class Carrito:
    elementos = []  # Compartido por todas las instancias

La alternativa correcta es crear la lista dentro de __init__:

class Carrito:
    def __init__(self) -> None:
        self.elementos = []

Métodos de clase

Un método de clase recibe la clase y puede funcionar como constructor alternativo:

class Producto:
    def __init__(self, nombre: str, precio: float) -> None:
        self.nombre = nombre
        self.precio = precio

    @classmethod
    def desde_texto(cls, valor: str):
        nombre, precio = valor.rsplit(",", maxsplit=1)
        return cls(nombre.strip(), float(precio))


producto = Producto.desde_texto("Manual, 39.90")

Utilizar cls permite que el constructor funcione correctamente con subclases.

Métodos estáticos

class Producto:
    @staticmethod
    def precio_valido(valor: float) -> bool:
        return valor >= 0

Un método estático pertenece conceptualmente a la clase, pero no necesita una instancia. Si la función no está estrechamente relacionada, una función normal del módulo puede resultar más clara.

Encapsulación en Python

Python utiliza convenciones:

  • nombre es público;
  • _nombre indica un detalle interno;
  • __nombre activa name mangling para reducir conflictos accidentales.

El guion bajo no es una barrera de seguridad. Comunica que el código externo debería usar la interfaz pública.

Validar con propiedades

class Producto:
    def __init__(self, nombre: str, precio: float) -> None:
        self.nombre = nombre
        self.precio = precio

    @property
    def precio(self) -> float:
        return self._precio

    @precio.setter
    def precio(self, valor: float) -> None:
        if valor < 0:
            raise ValueError("El precio no puede ser negativo")
        self._precio = float(valor)

El usuario mantiene una sintaxis de atributo normal y la clase aplica reglas. No necesitas propiedades cuando un atributo público sencillo es suficiente.

Herencia

class Empleado:
    def __init__(self, nombre: str) -> None:
        self.nombre = nombre

    def describir_rol(self) -> str:
        return "Empleado"


class Desarrollador(Empleado):
    def __init__(self, nombre: str, lenguaje: str) -> None:
        super().__init__(nombre)
        self.lenguaje = lenguaje

    def describir_rol(self) -> str:
        return f"Desarrollador de {self.lenguaje}"

super() llama a la implementación de la clase base. La subclase especializa el comportamiento.

Polimorfismo

class NotificacionEmail:
    def enviar(self, mensaje: str) -> None:
        print(f"Email: {mensaje}")


class NotificacionSMS:
    def enviar(self, mensaje: str) -> None:
        print(f"SMS: {mensaje}")


def notificar(canal, mensaje: str) -> None:
    canal.enviar(mensaje)

La función depende del comportamiento que necesita, no de una jerarquía concreta. Este enfoque se relaciona con el duck typing de Python.

Composición

class Motor:
    def arrancar(self) -> str:
        return "Motor arrancado"


class Coche:
    def __init__(self, modelo: str, motor: Motor) -> None:
        self.modelo = modelo
        self.motor = motor

    def arrancar(self) -> str:
        return f"{self.modelo}: {self.motor.arrancar()}"

Un coche tiene un motor, por lo que la composición expresa mejor la relación. Reserva la herencia para relaciones “es un” donde la sustitución tiene sentido.

Dataclasses

Cuando una clase almacena principalmente datos, dataclasses reduce código repetitivo:

from dataclasses import dataclass


@dataclass
class Cliente:
    nombre: str
    correo: str
    activo: bool = True


cliente = Cliente("Ana", "ana@example.com")
print(cliente)

Python genera __init__ y __repr__. La documentación oficial de dataclasses explica valores predeterminados y objetos inmutables.

Proyecto: cuenta bancaria

class CuentaBancaria:
    def __init__(
        self,
        titular: str,
        saldo_inicial: float = 0,
    ) -> None:
        if saldo_inicial < 0:
            raise ValueError("Saldo inicial inválido")
        self.titular = titular
        self._saldo = float(saldo_inicial)

    @property
    def saldo(self) -> float:
        return self._saldo

    def depositar(self, cantidad: float) -> None:
        if cantidad <= 0:
            raise ValueError("El depósito debe ser positivo")
        self._saldo += cantidad

    def retirar(self, cantidad: float) -> None:
        if cantidad <= 0:
            raise ValueError("El retiro debe ser positivo")
        if cantidad > self._saldo:
            raise ValueError("Saldo insuficiente")
        self._saldo -= cantidad

La clase mantiene sus reglas en un único lugar y protege el estado.

Probar objetos

import pytest


def test_deposito_aumenta_saldo():
    cuenta = CuentaBancaria("Ana", 100)
    cuenta.depositar(25)
    assert cuenta.saldo == 125


def test_retiro_rechaza_saldo_insuficiente():
    cuenta = CuentaBancaria("Ana", 50)

    with pytest.raises(ValueError, match="Saldo insuficiente"):
        cuenta.retirar(75)

La guía de Pytest explica pruebas de excepciones y fixtures.

Documentar clases

class CuentaBancaria:
    """Representa una cuenta con depósitos y retiros validados."""

    def depositar(self, cantidad: float) -> None:
        """Añade una cantidad positiva al saldo."""
        ...

La guía de docstrings en Python muestra cómo documentar clases, métodos y excepciones.

Cuándo utilizar clases

  • Datos y operaciones pertenecen al mismo concepto.
  • Existen varias instancias con estados independientes.
  • Las reglas deben cumplirse cada vez que cambia el estado.
  • Diferentes implementaciones comparten una interfaz.
  • La composición modela partes que cooperan.

Cuándo una clase es innecesaria

def celsius_a_fahrenheit(celsius: float) -> float:
    return celsius * 9 / 5 + 32

Una transformación pequeña y sin estado puede ser más clara como función. Python permite combinar estilos procedurales, funcionales y orientados a objetos.

Errores frecuentes

  • Olvidar self.
  • Usar atributos de clase para listas de instancia.
  • Crear jerarquías de herencia profundas.
  • Escribir getters y setters para todo.
  • Permitir que una clase tenga demasiadas responsabilidades.
  • No validar el estado.
  • Probar detalles internos en lugar de comportamiento público.
  • Confundir identidad e igualdad; consulta == frente a is en Python.

Conclusión

La programación orientada a objetos en Python combina estado y comportamiento mediante clases e instancias. Empieza con __init__, atributos y métodos. Añade propiedades cuando exista validación, utiliza composición para relaciones “tiene un” y aplica herencia cuando una subclase pueda sustituir realmente a la base. El objetivo no es crear la mayor cantidad posible de clases, sino representar conceptos y reglas de forma comprensible y comprobable.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Depuración de un proceso Python en terminal con código
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    pdb -p: depura procesos Python en ejecución

    Aprende a conectar pdb a un proceso Python en ejecución, inspeccionar la pila y diagnosticar bloqueos de forma segura.

    Ler mais

    Tempo de leitura: 6 minutos
    09/10/2026
    Código Python para representar fracciones exactas
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    fractions.from_number: convierte números en fracciones

    Aprende fractions.from_number en Python para convertir números en fracciones exactas, controlar precisión, validar entradas y evitar redondeos inesperados.

    Ler mais

    Tempo de leitura: 4 minutos
    09/10/2026
    Desarrollador configurando un servidor HTTPS y certificado TLS con Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    HTTPSServer: crea un servidor HTTPS local en Python

    Aprende HTTPSServer en Python para crear servicios HTTPS locales, configurar certificados, usar hilos y comprender sus límites.

    Ler mais

    Tempo de leitura: 4 minutos
    08/10/2026
    Archivos protegidos que representan extracción segura de TAR con Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    tarfile extraction_filter: extrae TAR con seguridad

    Aprende tarfile extraction_filter en Python para extraer archivos TAR con validación, seguridad y control de rutas.

    Ler mais

    Tempo de leitura: 6 minutos
    08/10/2026
    Código Python mostrando avisos controlados con catch_warnings
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    catch_warnings: captura warnings en pruebas Python

    Aprende catch_warnings en Python para capturar, probar y controlar avisos con filtros específicos y alcance seguro.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Código Python que representa referencias persistentes de pickle
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    pickle persistent_id: serializa referencias externas

    Aprende pickle persistent_id en Python para referencias externas estables, validación, seguridad, rendimiento y compatibilidad.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026