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.
