Clases en Python: __init__, variables de instancia vs clase y el error con mutables

Las clases de Python son más simples que en otros lenguajes orientados a objetos, pero tienen sus propias reglas y trampas. Conocer la diferencia entre variables de instancia y de clase, y el error con los mutables compartidos, te ahorra horas de depuración.

__init__ y self

__init__ es el inicializador (no el constructor). self es la referencia a la instancia concreta y siempre va como primer parámetro:

class Producto:
    def __init__(self, nombre, precio, stock=0):
        self.nombre = nombre    # variable de instancia
        self.precio = precio
        self.stock  = stock

    def aplicar_descuento(self, porcentaje):
        self.precio *= (1 - porcentaje / 100)
        return self

    def __str__(self):
        return f"{self.nombre}: {self.precio:.2f}€ ({self.stock} uds)"

p = Producto("Teclado", 89.99, stock=50)
p.aplicar_descuento(10)
print(p)  # Teclado: 80.99€ (50 uds)

Variables de instancia vs variables de clase

Las variables de clase se definen fuera de __init__ y son compartidas por todas las instancias. Las variables de instancia se definen con self. y son privadas de cada objeto:

class Contador:
    total = 0   # variable de clase: compartida por todas las instancias

    def __init__(self, nombre):
        Contador.total += 1
        self.nombre = nombre         # variable de instancia
        self.id     = Contador.total

c1 = Contador("primero")
c2 = Contador("segundo")
c3 = Contador("tercero")

print(Contador.total)   # 3
print(c1.id, c2.id)    # 1 2

El error clásico: mutable como variable de clase

# MAL: la lista es compartida por TODAS las instancias
class Carrito:
    items = []   # ? TRAMPA

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

c1 = Carrito()
c2 = Carrito()
c1.agregar("manzana")
print(c2.items)  # ['manzana'] — ¡el carrito de c2 tiene el item de c1!

# BIEN: inicializar en __init__
class Carrito:
    def __init__(self):
        self.items = []   # lista nueva por instancia

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

c1 = Carrito()
c2 = Carrito()
c1.agregar("manzana")
print(c2.items)  # [] — correcto

@classmethod y @staticmethod

class Temperatura:
    def __init__(self, celsius):
        self.celsius = celsius

    @classmethod
    def desde_fahrenheit(cls, f):
        """Constructor alternativo: crea desde Fahrenheit."""
        return cls((f - 32) * 5 / 9)

    @classmethod
    def desde_kelvin(cls, k):
        return cls(k - 273.15)

    @staticmethod
    def celsius_a_fahrenheit(c):
        """Conversión pura, sin necesitar instancia ni clase."""
        return c * 9 / 5 + 32

    def __str__(self):
        return f"{self.celsius:.1f}°C"

t1 = Temperatura.desde_fahrenheit(98.6)
print(t1)  # 37.0°C

print(Temperatura.celsius_a_fahrenheit(100))  # 212.0

__repr__ y __str__

class Punto:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        """Representación técnica: debería poder reconstruir el objeto."""
        return f"Punto({self.x!r}, {self.y!r})"

    def __str__(self):
        """Representación legible para el usuario."""
        return f"({self.x}, {self.y})"

p = Punto(3, 4)
print(p)        # (3, 4)         — usa __str__
print(repr(p))  # Punto(3, 4)   — usa __repr__
print([p])      # [Punto(3, 4)] — listas usan __repr__

Define siempre __repr__ primero: si no hay __str__, Python usa __repr__ en ambos casos. Usa @classmethod para constructores alternativos y @staticmethod para funciones de utilidad relacionadas con la clase pero que no necesitan acceder ni a la instancia ni a la clase.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP