Por defecto, Python asigna un diccionario __dict__ a cada instancia de una clase. Es flexible, sí. Pero cuando se tienen miles o millones de objetos en memoria al mismo tiempo, ese coste se acumula rápidamente. __slots__ elimina ese diccionario y reemplaza el almacenamiento por instancia con descriptores internos más compactos. Resultado típico: entre un 40 y un 60 por ciento menos de memoria por instancia.
Lo que hace Python sin slots
Sin ninguna declaración especial, cada instancia lleva su propio __dict__:
class Punto:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
p = Punto(1.0, 2.0)
print(p.__dict__) # {'x': 1.0, 'y': 2.0}
Este diccionario permite añadir atributos en tiempo de ejecución:
p.z = 3.0 # funciona
p.etiqueta = "A" # también funciona
Esa flexibilidad tiene un precio. Un dict vacío en Python ocupa varios cientos de bytes (184 bytes en Python 3.12, 232 en Python 3.8), y cada instancia paga ese coste.
slots: eliminar el dict
Declarar __slots__ le indica a Python el conjunto exacto de atributos que puede tener una instancia. Python omite por completo la asignación del __dict__ y utiliza descriptores de slot en su lugar:
class Punto:
__slots__ = ("x", "y")
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
p = Punto(1.0, 2.0)
print(hasattr(p, "__dict__")) # False
Intentar asignar un atributo no declarado lanza inmediatamente un AttributeError:
p.z = 3.0 # AttributeError: 'Punto' object has no attribute 'z'
Medir la ganancia real
Aquí hay un benchmark concreto usando tracemalloc:
import tracemalloc
class SinSlots:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
class ConSlots:
__slots__ = ("x", "y")
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
N = 100_000
tracemalloc.start()
sin_slots = [SinSlots(i, i) for i in range(N)]
_, pico_sin = tracemalloc.get_traced_memory()
tracemalloc.stop()
tracemalloc.start()
con_slots = [ConSlots(i, i) for i in range(N)]
_, pico_con = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"Sin __slots__: {pico_sin / 1_000_000:.1f} MB")
print(f"Con __slots__: {pico_con / 1_000_000:.1f} MB")
Salida típica en Python 3.12:
Sin __slots__: 28.3 MB
Con __slots__: 9.6 MB
Alrededor de un 66 por ciento de reducción. El número exacto varía según la cantidad de atributos y su tipo.
Cuándo usar slots
Grandes cantidades de instancias en memoria simultáneamente. Casos clásicos: objetos de datos para simulaciones, nodos de grafos o árboles, eventos en un pipeline de procesamiento, registros cargados en masa desde una API o un fichero plano.
Prevenir la creación accidental de atributos. En value objects y DTOs, prohibir los atributos dinámicos elimina una clase de bugs sutil: una errata en un nombre de atributo que crea silenciosamente un atributo nuevo en lugar de modificar el que se pretendía.
Velocidad de acceso a atributos. El acceso mediante descriptor es marginalmente más rápido que la búsqueda en tabla hash a través de __dict__. La diferencia rara vez importa en una petición web típica, pero se aprecia en benchmarks ajustados o bucles internos críticos.
Las trampas que conviene conocer
Herencia: clases padre sin slots
Si una clase padre no tiene __slots__, sus instancias llevan un __dict__, y las subclases lo heredan aunque declaren sus propios slots:
class Base:
pass # sin __slots__ → tiene __dict__
class Hijo(Base):
__slots__ = ("x",)
h = Hijo()
h.sorpresa = "sigue funcionando" # Base no tiene __slots__, sus instancias tienen __dict__ que Hijo hereda
Para que el beneficio de memoria se mantenga en toda la jerarquía, cada clase de la cadena debe declarar sus propios __slots__ sin heredar de una clase que tenga __dict__.
Incompatibilidades con frameworks
Varias bibliotecas esperan encontrar un __dict__ en las instancias: algunos ORMs, frameworks de serialización, e implementaciones de pickle o copia profunda según su versión. Verificar la compatibilidad antes de aplicar __slots__ a clases usadas con frameworks de terceros.
weakref también desaparece
Las instancias con __slots__ pierden el soporte para referencias débiles por defecto. Si se necesita, añadir "__weakref__" explícitamente a la lista:
class Nodo:
__slots__ = ("valor", "siguiente", "__weakref__")
dataclasses y slots
Desde Python 3.10, @dataclass acepta slots=True directamente:
from dataclasses import dataclass
@dataclass(slots=True)
class Coordenada:
x: float
y: float
z: float
Esta es la forma más limpia de usar __slots__ en clases de datos modernas. Sin declaración manual, sin riesgo de que la lista de slots quede desincronizada con los campos.
Resumen
| Sin slots | Con slots | |
|---|---|---|
| Almacenamiento de atributos | __dict__ por instancia | Descriptores internos |
| Añadir atributos dinámicos | Sí | No |
| Memoria por instancia | Mayor | 40 a 60% menos |
| Velocidad de acceso | Ligeramente más lento | Ligeramente más rápido |
| Compatibilidad con frameworks | Universal | Hay que verificar |
| Herencia multinivel | Simple | Requiere disciplina |
__slots__ no es la primera optimización a la que recurrir. Es una herramienta de precisión para situaciones donde la memoria ha sido medida y documentada como un problema real. Cientos de objetos en una aplicación web típica: impacto despreciable. Millones de objetos en memoria durante una ejecución de pipeline o una simulación: uno de los palancas más directas disponibles en Python puro.
