2.3Programación orientada a objetos (clases, herencia, dunder)
Lo esencial en 30 segundos
- La clase es el molde, el objeto la pieza: atributos (datos) + métodos (verbos sobre self) viajan juntos — se acabaron los dicts + funciones dispersas.
- Herencia = es-un: el hijo especializa al padre vía super(), poco profunda (≤ 3) — composición para tiene-un.
- Dunders = objetos ciudadanos: __str__/__eq__/__len__ responden a print/==/len — @dataclass para los portadatos.
Minicaso hilado — Nabil modela sus ventas en objetos
Los dicts de Nabil se dispersan: funciones que manipulan ventas sin verificar nunca los montos, informes que duplican los mismos cálculos. Modela: una clase Venta (wilaya, monto, métodos total y __str__ legible), una clase InformeMensual que agrupa las ventas, y luego una clase VentaDescuento que HEREDA de Venta y añade una tasa (super().__init__() reutiliza al padre). Se acabaron los dicts + funciones desperdigadas: datos y comportamientos viven juntos. En 2.4, la creación rechazará los montos negativos con raise.
Continuación del hilo 2.2: las líneas de Nabil se convierten en objetos — y continúa en 2.4.
1. La clase: el molde, el objeto: la pieza
class Venta: + __init__(self, ...) define el molde; v = Venta("Argel", 1200) crea la pieza. self designa el objeto mismo en sus métodos: cada objeto accede a SUS atributos (self.monto) con independencia de los demás. Los atributos son los datos, los métodos son los comportamientos (verbos sobre self, nunca global). Datos + comportamientos viajan juntos: se acabaron los diccionarios + funciones dispersas. Nabil crea así dos ventas distintas que nunca se mezclan.
2. Herencia: especializar, no copiar
class VentaDescuento(Venta): hereda y especializa (tasa, cálculo descontado). super().__init__() reutiliza el constructor del padre en lugar de recopiarlo; redefinir un método en el hijo es el polimorfismo. Regla: herencia para un «es-un» (una VentaDescuento es-una Venta), composición («tiene-un»: un Informe tiene-unas Ventas) si no. Tres niveles como máximo: más allá, es un laberinto. Nabil añade así las ventas con descuento sin tocar el código existente.
3. Dunder: hablar python con fluidez
Los métodos dunder (doble guion bajo) integran sus objetos en los mecanismos nativos: __str__ para una visualización legible (print(v)), __eq__ para la igualdad (==), __len__ para el tamaño (len()). Sin ellos, print(objeto) muestra un críptico <... object at 0x...>. Para los portadatos simples, @dataclass genera __init__, __eq__ y __repr__ automáticamente. Los objetos de Nabil responden así a print() y == como los tipos nativos.
🔎 Ejemplo concreto — 🔎 Ejemplo concreto — Las ventas de Nabil que se muestran
Objetos legibles y comparables, como en casa de Nabil:
class Venta:
def __init__(self, wilaya, monto):
self.wilaya = wilaya
self.monto = float(monto)
def __str__(self):
return f"{self.wilaya} : {self.monto:.2f} €"
class VentaDescuento(Venta):
def __init__(self, wilaya, monto, tasa=0.1):
super().__init__(wilaya, monto)
self.tasa = tasa
def neto(self):
return self.monto * (1 - self.tasa)
v = VentaDescuento("Argel", 1000)
print(v) # Argel : 1000.00 €
print(f"Neto : {v.neto():.2f} €") # Neto : 900.00 €
Lectura: __init__ inicializa, __str__ muestra, super() reutiliza al padre.
Lección: métodos = verbos sobre self, nunca global.
Vocabulario de la sección
- Clase / objeto
- La clase es el plano (molde); el objeto (instancia) es una realización concreta creada a partir de ese plano: v = Venta(...).
Ejemplo sencillo: Nabil crea dos ventas distintas desde la misma clase.
Lo que no es: un diccionario libre sin reglas ni comportamientos.
Reflejo: concepto para modelar = clase.
- Atributo / método
- El atributo es un dato del objeto (self.monto); el método es una función ligada al objeto, verbo sobre self.
Ejemplo sencillo: La venta de Nabil lleva wilaya y calcula neto().
Lo que no es: unas funciones globales dispersas lejos de los datos.
Reflejo: verbo sobre los datos = método.
- __init__ / self
- __init__ es el constructor, llamado en la creación; self designa el objeto actual, escrito en la definición pero pasado automáticamente en la llamada.
Ejemplo sencillo: Venta("Argel", 1200) inicializa self.wilaya y self.monto.
Lo que no es: un parámetro para pasar a mano en cada llamada.
Reflejo: crear un objeto = __init__ vía self.
- Herencia
- Una clase hija hereda los atributos y métodos de un padre y los especializa; super() reutiliza al padre. Reservada al «es-un», poco profunda.
Ejemplo sencillo: VentaDescuento hereda de Venta y añade la tasa.
Lo que no es: un copiar-pegar de la clase padre.
Reflejo: es-un = herencia; tiene-un = composición.
- Método dunder
- Método de doble guion bajo (__str__, __len__, __eq__) que conecta sus objetos con print(), ==, len(); @dataclass los genera para los portadatos.
Ejemplo sencillo: print(venta) muestra «Argel: 1000.00 €» gracias a __str__.
Lo que no es: una visualización críptica
Reflejo: objeto mostrable/comparable = dunder.
En la práctica — En práctica — Crear clases y herencia
- Defina una clase Venta con __init__(self, wilaya, monto) y un método mostrar(). Cree dos objetos distintos y verifique su independencia.
- Añada __str__ para una visualización legible (print(v) → «Argel: 1000.00 €») y __eq__ para comparar dos ventas.
- Cree una clase VentaDescuento que HEREDE de Venta, añada una tasa y un método neto(). Reutilice al padre con super().__init__().
- Redefina neto() en el hijo (polimorfismo) y pruebe @dataclass en una clase portadatos simple.
Script listo para decir — defender la clase ante el equipo (30 s)
«Para las ventas de Nabil, he sustituido los dicts dispersos por una clase Venta: wilaya y monto inicializados en __init__, cálculos en métodos sobre self — cada objeto es independiente. Los descuentos heredan con super(), y __str__ da una visualización limpia. Ya no se tocan los datos sin pasar por el objeto.»
Para decir mostrando un dict improvisado y luego el mismo caso en clase.
Modelo copiar-pegar — clase, herencia y dunder
class Venta:
def __init__(self, wilaya, monto):
if float(monto) < 0:
raise ValueError("monto negativo prohibido")
self.wilaya = wilaya
self.monto = float(monto)
def __str__(self):
return f"{self.wilaya} : {self.monto:.2f} €"
def __eq__(self, otro):
if not isinstance(otro, Venta):
return NotImplemented
return (self.wilaya, self.monto) == (otro.wilaya, otro.monto)
class VentaDescuento(Venta):
def __init__(self, wilaya, monto, tasa=0.1):
super().__init__(wilaya, monto)
self.tasa = tasa
def neto(self):
return self.monto * (1 - self.tasa)
Venta simple
Una Venta de Nabil lleva wilaya + monto y se muestra en f-string vía __str__.
Venta con descuento
VentaDescuento hereda y añade la tasa: super() reutiliza al padre, neto() calcula.
Informe mensual
El Informe tiene-unas Ventas (composición): totaliza sin heredar — tiene-un, no es-un.
Portadatos
Un resumen simple se convierte en @dataclass: __init__ y __eq__ generados, cero código repetitivo.
✓ Buenas prácticas
- Agrupar datos + comportamientos en clase (nunca dict + funciones dispersas).
- Heredar poco profundo (≤ 3) para es-un; componer para tiene-un; super() para reutilizar.
- Añadir __str__/__eq__ y @dataclass para objetos ciudadanos.
✗ Errores frecuentes
- Mantener todo en dict + funciones globales dispersas.
- Usar la herencia para un «tiene-un» en lugar de la composición.
- Apilar 6 niveles de herencia ilegibles.
Puntos clave
- La clase es el molde; el objeto (instancia) es una realización.
- __init__ inicializa el objeto; self designa el objeto actual (pasado automáticamente).
- La herencia permite a una clase hija reutilizar/extender una clase padre (es-un, super()).
- Los métodos dunder (__str__… ) integran sus objetos en los mecanismos de Python; @dataclass para los portadatos.
Desafío 48h — aplíquelo mañana
Modele un concepto real (el de Nabil o el suyo) en clase con __init__ + __str__, cree dos objetos independientes, añada una clase hija con super() y pruebe la igualdad con __eq__.
- Puedo explicar clase frente a objeto y el papel de self.
- Sé crear una clase, un método y una herencia con super().
- He hecho mis objetos legibles con __str__ (y @dataclass si conviene).
Preguntas frecuentes
¿Para qué sirve self exactamente?
self representa el objeto sobre el que se llama al método. Permite a cada objeto acceder a SUS propios atributos (self.monto) con independencia de los demás. Python lo pasa automáticamente en la llamada: lo escribe en la definición, no en la llamada.
¿La POO es indispensable para empezar?
Para pequeños scripts, no: las funciones bastan. Pero en cuanto los programas crecen — y para entender las bibliotecas (pandas, Django…) que la usan en todas partes — la POO se vuelve esencial. Invertir aquí renta a largo plazo.
¿Qué designa «self» en un método?