Lección 9 de 12 · Nivel 3: Avanzado
Clases y objetos en Python: programación orientada a objetos
En esta lección
- Entender qué son las clases y los objetos, y cuándo conviene usarlos.
- Crear tus propias clases con atributos y métodos.
- Usar herencia y los métodos especiales
__str__y__init__. - Escribir clases de datos en pocas líneas con
dataclass.
Bienvenido al nivel avanzado. Hasta ahora organizaste tus programas con funciones y guardaste los datos en diccionarios. Funciona, pero cuando un programa crece aparecen problemas: diccionarios con claves mal escritas, funciones sueltas que operan sobre los mismos datos, lógica repetida. La programación orientada a objetos (POO) agrupa los datos y las funciones que los manejan en una misma unidad: el objeto. Es la forma en que están escritas casi todas las librerías que usarás.
Clase y objeto
- Una clase es un molde: define qué datos tiene algo y qué puede hacer. Por ejemplo, "Cuenta bancaria".
- Un objeto (o instancia) es un caso concreto creado con ese molde: la cuenta de Ana, la cuenta de Luis.
De hecho, ya usaste objetos sin saberlo: cada texto es un objeto de la clase str, y por eso tiene métodos como .upper().
Tu primera clase
class CuentaBancaria:
def __init__(self, titular, saldo=0):
self.titular = titular
self.saldo = saldo
def depositar(self, monto):
self.saldo += monto
def retirar(self, monto):
if monto > self.saldo:
print("Saldo insuficiente")
return False
self.saldo -= monto
return True
cuenta_ana = CuentaBancaria("Ana", 500)
cuenta_luis = CuentaBancaria("Luis")
cuenta_ana.depositar(200)
cuenta_ana.retirar(1000) # Saldo insuficiente
print(cuenta_ana.saldo) # 700
print(cuenta_luis.saldo) # 0
Las piezas clave:
class CuentaBancaria:define la clase. Por convención, los nombres de clase van en CamelCase (palabras juntas con mayúscula inicial).__init__es el constructor: se ejecuta al crear el objeto y prepara sus datos.selfes el propio objeto.self.saldoes "el saldo de esta cuenta". Python lo pasa automáticamente: escribescuenta_ana.depositar(200), nodepositar(cuenta_ana, 200).- Atributos (
titular,saldo): los datos de cada objeto. - Métodos (
depositar,retirar): las funciones que pertenecen a la clase.
1. ¿Para qué sirve self dentro de un método?
self.saldo es el saldo de ese objeto en particular; cada cuenta tiene el suyo.
Mostrar objetos de forma legible: __str__
Si haces print(cuenta_ana) verás algo como <__main__.CuentaBancaria object at 0x7f...>. Para controlar cómo se muestra, define __str__:
class CuentaBancaria:
def __init__(self, titular, saldo=0):
self.titular = titular
self.saldo = saldo
def __str__(self):
return f"Cuenta de {self.titular}: {self.saldo:,.2f}"
print(CuentaBancaria("Ana", 1250.5)) # Cuenta de Ana: 1,250.50
Los métodos con doble guion bajo (dunder methods) le dicen a Python cómo debe comportarse tu objeto en operaciones especiales: mostrarse, compararse, sumarse, medirse con len()…
Encapsulamiento: proteger los datos
¿Qué impide que alguien escriba cuenta_ana.saldo = -5000? Nada, técnicamente. Python confía en convenciones: un atributo que empieza con guion bajo (_saldo) significa "es interno, no lo toques desde fuera". Y con @property puedes exponer un valor de solo lectura:
class CuentaBancaria:
def __init__(self, titular, saldo=0):
self.titular = titular
self._saldo = saldo
self._movimientos = []
@property
def saldo(self):
return self._saldo
def depositar(self, monto):
if monto <= 0:
raise ValueError("El depósito debe ser positivo")
self._saldo += monto
self._movimientos.append(("depósito", monto))
c = CuentaBancaria("Ana")
c.depositar(100)
print(c.saldo) # 100
# c.saldo = 999 → AttributeError: no se puede asignar
Observa raise ValueError(...): en lugar de imprimir un mensaje, el método lanza una excepción, que quien lo llame puede capturar con try/except (lección 8). Es la forma profesional de señalar un error.
2. ¿Qué indica, por convención, un atributo llamado _saldo?
El guion bajo inicial es una convención de "privado". Python no lo impide, pero cualquier programador entiende el mensaje.
Herencia: reutilizar y especializar
Una clase puede heredar de otra: obtiene todos sus atributos y métodos, y puede añadir o cambiar algunos.
class CuentaAhorro(CuentaBancaria):
def __init__(self, titular, saldo=0, tasa_anual=0.05):
super().__init__(titular, saldo)
self.tasa_anual = tasa_anual
def aplicar_interes_mensual(self):
interes = self._saldo * ((1 + self.tasa_anual) ** (1 / 12) - 1)
self.depositar(round(interes, 2))
ahorro = CuentaAhorro("Luis", 10000, tasa_anual=0.06)
ahorro.aplicar_interes_mensual()
print(ahorro.saldo) # 10048.68
class CuentaAhorro(CuentaBancaria): hereda deCuentaBancaria.super().__init__(...)llama al constructor de la clase padre para no repetir código.- La cuenta de ahorro puede usar
depositarysaldosin haberlos escrito.
Usa la herencia cuando la relación sea "es un tipo de": una cuenta de ahorro es un tipo de cuenta bancaria. Si la relación es "tiene un" (un cliente tiene cuentas), es mejor guardar un objeto dentro de otro (composición).
3. ¿Qué hace super().__init__(titular, saldo) en la clase hija?
Así la clase hija reutiliza la inicialización del padre y solo añade lo suyo (tasa_anual).
dataclass: clases de datos sin escribir de más
Cuando una clase sirve sobre todo para guardar datos, el decorador @dataclass genera automáticamente __init__, una representación legible y la comparación entre objetos:
from dataclasses import dataclass
from datetime import date
@dataclass
class Gasto:
fecha: date
categoria: str
descripcion: str
monto: float
def es_grande(self, limite=100):
return self.monto > limite
g = Gasto(date(2026, 10, 1), "comida", "Almuerzo", 35.5)
print(g) # Gasto(fecha=datetime.date(2026, 10, 1), categoria='comida', ...)
print(g.es_grande()) # False
Compáralo con los diccionarios del proyecto de la lección 8: ahora un error al escribir un campo (g.categria) da un error claro en lugar de pasar desapercibido, y tu editor te autocompleta los nombres.
¿Cuándo usar clases?
- Cuando tienes varios datos que siempre van juntos y operaciones sobre ellos (una cuenta, un pedido, un producto).
- Cuando necesitas varios objetos del mismo tipo con su propio estado.
- Cuando quieres variantes de algo (herencia).
Y cuándo no: para un script corto que procesa un archivo, unas pocas funciones bastan. No todo tiene que ser una clase.
Reto: carrito de compras
Crea una clase Producto (con @dataclass: nombre y precio) y una clase Carrito con los métodos:
agregar(producto, cantidad=1)total(), que devuelva el totalaplicar_descuento(porcentaje), que lanceValueErrorsi el porcentaje no está entre 0 y 50__str__, que muestre el detalle del carrito
Ver solución
from dataclasses import dataclass
@dataclass
class Producto:
nombre: str
precio: float
class Carrito:
def __init__(self):
self._items = [] # lista de (producto, cantidad)
self._descuento = 0
def agregar(self, producto, cantidad=1):
self._items.append((producto, cantidad))
def aplicar_descuento(self, porcentaje):
if not 0 <= porcentaje <= 50:
raise ValueError("Descuento no válido")
self._descuento = porcentaje
def total(self):
bruto = sum(p.precio * c for p, c in self._items)
return round(bruto * (1 - self._descuento / 100), 2)
def __str__(self):
lineas = [f"{c} x {p.nombre}: {p.precio * c:.2f}" for p, c in self._items]
lineas.append(f"TOTAL: {self.total():.2f}")
return "\n".join(lineas)
carrito = Carrito()
carrito.agregar(Producto("Teclado", 25.0), 2)
carrito.agregar(Producto("Mouse", 12.5))
carrito.aplicar_descuento(10)
print(carrito) # ... TOTAL: 56.25
Resumen
- Una clase es un molde; un objeto, un caso concreto con sus propios datos.
__init__inicializa,selfes el objeto,__str__define cómo se muestra.- Usa
_atributoy@propertypara proteger datos yraisepara señalar errores. - Herencia para "es un tipo de";
@dataclasspara clases de datos.
En la próxima lección organizarás tus programas como un profesional: módulos, paquetes, pip y entornos virtuales.
Crea tu cuenta, marca tus avances y descarga el certificado al terminar el curso.