2.3 · Programación orientada a objetos (clases, herencia, dunder)

Nivel 2 · Estructuras de datos y POO

2.3Programación orientada a objetos (clases, herencia, dunder)

Objetivo: Crear clases, la herencia y los métodos especiales (__init__, __str__… ).
Tiempo estimado: 18 min · lectura autónoma · Si solo tienes 5 min: lee «Lo esencial en 30s» + el esquema.

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.

Error frecuente de interpretación: «La POO son solo dicts con funciones dentro — y self no sirve para nada». No. Un dict no protege nada (claves libres, ninguna validación, funciones dispersas); una clase agrupa datos + comportamientos, inicializa en __init__ y aísla cada objeto vía self — dos objetos nunca se mezclan. Olvidar self es compartir o perder el estado. La regla de Nabil: datos que viajan con sus reglas = clase; self se escribe en la definición, nunca en la llamada (Python lo pasa solo).
Objetos limpios Datos y comportamientos juntos Clase (molde)__init__ + self — atributos y métodos+Herencia (es-un)super(), especializar, ≤ 3 niveles+Dunder (__str__…)print, ==, len como los nativos Herencia para es-un, composición para tiene-un.
Datos y comportamientos juntos
Versión en texto del esquema (SEO / lectores de pantalla / traducción): 1) Nabil define el MOLDE (class Venta + __init__) y crea PIEZAS (objetos): self aísla cada objeto, atributos + métodos viajan juntos. 2) ESPECIALIZA por herencia (VentaDescuento es-una Venta, super() reutiliza al padre, ≤ 3 niveles) y compone si no (un Informe tiene-unas Ventas). 3) Convierte sus objetos en CIUDADANOS con los dunders (__str__ para print, __eq__ para ==) y @dataclass para los portadatos. Lección: modelar en objetos en lugar de dispersar dicts y funciones.

🔎 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

  1. Defina una clase Venta con __init__(self, wilaya, monto) y un método mostrar(). Cree dos objetos distintos y verifique su independencia.
  2. Añada __str__ para una visualización legible (print(v) → «Argel: 1000.00 €») y __eq__ para comparar dos ventas.
  3. Cree una clase VentaDescuento que HEREDE de Venta, añada una tasa y un método neto(). Reutilice al padre con super().__init__().
  4. Redefina neto() en el hijo (polimorfismo) y pruebe @dataclass en una clase portadatos simple.
Modela conceptos en clases, crea objetos, usa la herencia y hace sus objetos legibles con __str__: los fundamentos de la POO.

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__.

Lista de salida de la sección
  • 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).
Autoevaluación 1-5: ¿mis datos viajan con sus comportamientos?¿mi herencia sigue siendo poco profunda y justificada?

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.

Comprueba tu comprensión

¿Qué designa «self» en un método?

Recursos adicionales (opcional, para ir más allá)

Examen del nivel 2Preguntas inéditas y más difíciles para validar tus conocimientos

GUIDINEE.COM · Python