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 = precioUn 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 instanciasLa 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 >= 0Un 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:
nombrees público;_nombreindica un detalle interno;__nombreactiva 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", "[email protected]")
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 -= cantidadLa 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 + 32Una 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.


