Kodokon kodokon.com

POO: clases, __init__, métodos y herencia

Estructura tu código con clases bien diseñadas: inicialización, atributos de instancia o de clase, herencia y sus compromisos.

10 min · 3 preguntas

Abrir esta lección en Kodokon

Trabajas con objetos de Python todo el tiempo: cadenas, listas, archivos. Escribir tus propias clases significa reunir el estado (los atributos) y el comportamiento (los métodos) detrás de una interfaz coherente. Un punto de vocabulario que a menudo se malinterpreta: __init__ no es un constructor. El objeto ya existe (creado por __new__) cuando __init__ recibe self; su trabajo es inicializar la instancia. Y self no tiene nada de mágico: es el primer parámetro de todo método de instancia, que Python pasa automáticamente en el momento de la llamada.

PYTHON
class Account:
    def __init__(self, owner, balance=0.0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("amount must be > 0")
        self.balance += amount
        return self.balance

    def __repr__(self):
        return f"Account({self.owner!r}, {self.balance})"

account = Account("Ada")
account.deposit(120.0)
print(account)
Una clase completa: __init__, un método de negocio y __repr__

Una distinción crucial en producción: los atributos de clase se declaran en el cuerpo de la clase y los comparten todas las instancias; los atributos de instancia se crean en self, normalmente en __init__. Al leer, Python busca primero en la instancia, luego recurre a la clase y después a las clases padre. Una asignación self.x = ... siempre crea un atributo de instancia, que oculta el atributo de clase del mismo nombre. La trampa clásica del mundo real: un atributo de clase mutable.

PYTHON
class Basket:
    items = []

    def add(self, item):
        self.items.append(item)

a = Basket()
b = Basket()
a.add("apple")
print(b.items)  # ['apple']: shared list!
La trampa del atributo de clase mutable

La herencia expresa una relación "es un": un Car es un Vehicle. La subclase hereda los métodos, puede sobrescribirlos, y super() le permite llamar a la versión del padre - indispensable en __init__ para no cortocircuitar la inicialización del padre. Python resuelve las llamadas siguiendo el MRO (Method Resolution Order, el orden de resolución de métodos). No recurras a la herencia de forma sistemática: para reutilizar código sin una relación "es un", prefiere la composición (un objeto posee a otro). Las jerarquías profundas son frágiles: cada clase padre se convierte en una API implícita para todos sus hijos.

PYTHON
class Vehicle:
    def __init__(self, brand):
        self.brand = brand

    def describe(self):
        return self.brand

class Car(Vehicle):
    def __init__(self, brand, doors):
        super().__init__(brand)
        self.doors = doors

    def describe(self):
        base = super().describe()
        return f"{base}, {self.doors} doors"

print(Car("Aster", 5).describe())
Herencia y super(): extender sin reescribir

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. ¿Cuál es exactamente el papel de __init__?
    • Crear el objeto en memoria
    • Inicializar una instancia ya creada
    • Declarar los atributos de clase
    • Ser llamado en cada acceso a un atributo
  2. Una clase declara items = [] en su cuerpo. Después de a.add(1), que ejecuta self.items.append(1), ¿qué ve otra instancia b?
    • b.items es una lista vacía
    • b.items contiene 1: la lista se comparte
    • Python lanza un AttributeError
  3. En Car.__init__, ¿cuál es el sentido de la llamada super().__init__(brand)?
    • Crear una segunda instancia de Vehicle
    • Ejecutar la inicialización definida en la clase padre
    • Copiar los métodos de Vehicle dentro de Car
    • Comprobar que Car realmente hereda de Vehicle