26/02/2006
En el universo de Python, cada objeto que creamos viene con una mochila de funcionalidades y atributos estándar. Por defecto, Python utiliza un diccionario, conocido como __dict__, para almacenar los atributos de una instancia de clase. Esta característica es increíblemente flexible, ya que nos permite añadir nuevos atributos sobre la marcha, en tiempo de ejecución. Sin embargo, esta conveniencia tiene un coste oculto: la memoria. Los diccionarios pueden consumir una cantidad significativa de RAM, especialmente cuando se trabaja con miles o millones de instancias de objetos. Si el rendimiento y la eficiencia de la memoria son críticos para tu aplicación, existe una herramienta poderosa que te permite cambiar esa flexibilidad por una optimización considerable: el atributo __slots__.

Este artículo es una inmersión profunda en el mundo de __slots__. Exploraremos qué es, cómo funciona, sus ventajas en el consumo de memoria y velocidad, y las consideraciones que debes tener en cuenta antes de implementarlo en tus proyectos. Prepárate para llevar la eficiencia de tus clases a un nuevo nivel.
¿Qué es exactamente __slots__ en Python?
El atributo __slots__ es una variable de clase especial que permite declarar explícitamente los atributos que una instancia tendrá. Al definir __slots__, le indicamos a Python que no cree el diccionario __dict__ para cada objeto. En su lugar, reserva un espacio fijo en memoria para los atributos definidos, utilizando una estructura interna más compacta, similar a una tupla.
Sus dos propósitos principales son:
- Optimización de memoria: Al evitar la creación de un
__dict__por cada instancia, el ahorro de memoria puede ser sustancial, sobre todo en aplicaciones que manejan una gran cantidad de objetos pequeños. - Restricción de atributos: Impide la creación de nuevos atributos que no estén definidos en la lista de
__slots__. Esto puede ser útil para crear clases más robustas y prevenir errores por asignaciones accidentales.
La sintaxis es sencilla: se define como una secuencia (generalmente una tupla) de cadenas que contienen los nombres de los atributos de instancia.
Implementación Básica: Manos a la Obra
Veamos un ejemplo simple para entender su comportamiento. Crearemos una clase Punto para representar coordenadas en un plano 2D, primero sin __slots__ y luego con ellos.
Clase Tradicional con __dict__
class PuntoNormal: def __init__(self, x, y): self.x = x self.y = y p1 = PuntoNormal(10, 20) print(p1.x, p1.y) # Salida: 10 20 # Podemos añadir atributos dinámicamente p1.z = 30 print(p1.z) # Salida: 30 # Y podemos ver su diccionario de atributos print(p1.__dict__) # Salida: {'x': 10, 'y': 20, 'z': 30}Clase Optimizada con __slots__
Ahora, reescribamos la misma clase utilizando __slots__.
class PuntoConSlots: __slots__ = ('x', 'y') # Declaramos los únicos atributos permitidos def __init__(self, x, y): self.x = x self.y = y p2 = PuntoConSlots(10, 20) print(p2.x, p2.y) # Salida: 10 20 # Intentemos añadir un nuevo atributo try: p2.z = 30 except AttributeError as e: print(e) # Salida: 'PuntoConSlots' object has no attribute 'z' # Intentemos acceder a __dict__ try: print(p2.__dict__) except AttributeError as e: print(e) # Salida: 'PuntoConSlots' object has no attribute '__dict__'Como se puede observar, la clase con __slots__ no solo impide la creación del atributo z, sino que confirma que ya no posee un atributo __dict__. Aquí reside la clave de su eficiencia.
El Verdadero Poder: Benchmarking de Memoria y Velocidad
La diferencia teórica está clara, pero ¿cuál es el impacto real? Realicemos una pequeña prueba para comparar el uso de memoria y la velocidad de acceso a los atributos para una gran cantidad de instancias.
Utilizaremos los módulos sys para medir el tamaño en memoria y timeit para medir el tiempo de ejecución.

import sys import timeit class PuntoNormal: def __init__(self, x, y): self.x = x self.y = y class PuntoConSlots: __slots__ = ('x', 'y') def __init__(self, x, y): self.x = x self.y = y # Creación de un gran número de instancias num_instancias = 100000 instancias_normales = [PuntoNormal(i, i) for i in range(num_instancias)] instancias_slots = [PuntoConSlots(i, i) for i in range(num_instancias)] # Cálculo de memoria memoria_normal = sum(sys.getsizeof(p) + sys.getsizeof(p.__dict__) for p in instancias_normales) memoria_slots = sum(sys.getsizeof(p) for p in instancias_slots) print(f"Memoria sin __slots__: {memoria_normal / 1024:.2f} KB") print(f"Memoria con __slots__: {memoria_slots / 1024:.2f} KB") # Medición de velocidad de acceso tiempo_normal = timeit.timeit('[p.x for p in instancias]', globals={'instancias': instancias_normales}, number=100) tiempo_slots = timeit.timeit('[p.x for p in instancias]', globals={'instancias': instancias_slots}, number=100) print(f"Tiempo de acceso sin __slots__: {tiempo_normal:.6f} segundos") print(f"Tiempo de acceso con __slots__: {tiempo_slots:.6f} segundos")Tabla Comparativa de Resultados
Aunque los resultados exactos pueden variar según la versión de Python y la arquitectura del sistema, la tendencia general es clara.
| Métrica | Clase Normal (__dict__) | Clase con __slots__ | Mejora |
|---|---|---|---|
| Uso de Memoria | ~15-16 MB | ~5-6 MB | ~60-70% menos memoria |
| Velocidad de Acceso | Ligeramente más lento | Ligeramente más rápido | ~10-20% más rápido |
El ahorro de memoria es la ganancia más significativa, haciendo de __slots__ una herramienta indispensable para aplicaciones de alto rendimiento que manejan grandes volúmenes de datos en memoria.
Herencia y __slots__: Lo que Debes Saber
El comportamiento de __slots__ con la herencia tiene sus particularidades. Si una clase hija hereda de una clase base que usa __slots__, la clase hija no heredará automáticamente el comportamiento de los slots. De hecho, por defecto, la clase hija tendrá un __dict__, anulando los beneficios de memoria.
Para mantener la optimización, la clase hija también debe definir su propio atributo __slots__. Si la clase hija no define nuevos atributos, puede definir un __slots__ = () vacío.
class Base: __slots__ = ('a',) class HijaConDict(Base): # No define __slots__, por lo tanto tendrá __dict__ pass class HijaConSlots(Base): # Define sus propios slots para mantener la optimización __slots__ = ('b',) obj1 = HijaConDict() obj1.a = 1 obj1.c = 3 # Funciona, porque tiene __dict__ print('__dict__' in dir(obj1)) # Salida: True obj2 = HijaConSlots() obj2.a = 1 # 'a' es heredado de la base obj2.b = 2 # 'b' es propio de la hija try: obj2.c = 3 # Falla, no está en los slots de la base ni de la hija except AttributeError as e: print(e) print('__dict__' in dir(obj2)) # Salida: FalseCasos de Uso Avanzados
Uso con Referencias Débiles (__weakref__)
Normalmente, el soporte para referencias débiles (weak references) está habilitado a través del __dict__. Si necesitas utilizar weakref en una clase con __slots__, debes incluir explícitamente la cadena '__weakref__' en la tupla de slots.
import weakref class ObjetoReferenciable: __slots__ = ('dato', '__weakref__') def __init__(self, dato): self.data = dato obj = ObjetoReferenciable(100) ref = weakref.ref(obj) print(ref().data) # Salida: 100Combinando __slots__ con Propiedades (@property)
__slots__ funciona perfectamente con descriptores como las propiedades. Las propiedades son atributos de clase, no de instancia, por lo que no necesitan ser incluidas en la lista de __slots__. Solo los atributos que almacenarán datos en la instancia deben estar en la lista.
class Temperatura: __slots__ = ('_grados_celsius',) def __init__(self, celsius): self._grados_celsius = celsius @property def fahrenheit(self): return (self._grados_celsius * 9/5) + 32 temp = Temperatura(25) # _grados_celsius está en los slots y consume poca memoria print(temp._grados_celsius) # Salida: 25 # fahrenheit es una propiedad calculada, no ocupa espacio en la instancia print(temp.fahrenheit) # Salida: 77.0Preguntas Frecuentes (FAQ)
- ¿Cuándo debería usar __slots__?
- Deberías considerar usar
__slots__principalmente en situaciones donde la optimización de memoria es crucial. Esto ocurre típicamente cuando tu programa necesita crear un número muy grande (cientos de miles o millones) de instancias de una misma clase. No lo uses como una optimización prematura; primero, identifica cuellos de botella de memoria con herramientas de profiling. - ¿Pierdo alguna funcionalidad al usar __slots__?
- Sí, la principal pérdida es la flexibilidad. No puedes añadir nuevos atributos a una instancia dinámicamente. Esto puede complicar ciertas técnicas de programación como el monkey patching. También pierdes el
__dict__, que algunas librerías externas podrían esperar que exista. - ¿Qué pasa si una clase hija no define __slots__ pero su clase padre sí?
- La clase hija tendrá un
__dict__para sus propios atributos, pero los atributos definidos en los__slots__de la clase padre seguirán siendo gestionados por el mecanismo de slots. Esto resulta en un objeto híbrido que no es tan eficiente en memoria como una clase que solo usa slots. - ¿Puedo usar un tipo mutable, como una lista o un diccionario, en un slot?
- Sí, puedes asignar cualquier tipo de objeto a un atributo definido en
__slots__. Sin embargo, si asignas un diccionario grande a un slot, podrías contrarrestar parte del ahorro de memoria que estabas buscando.
Conclusión
El atributo __slots__ es una herramienta de optimización potente pero específica en el arsenal de un programador de Python. No es una solución que deba aplicarse a todas las clases por defecto, pero en los escenarios correctos, puede marcar una diferencia abismal en el rendimiento y el consumo de memoria de una aplicación. Al comprender su funcionamiento, sus beneficios y sus limitaciones, puedes tomar decisiones informadas sobre cuándo y cómo implementarlo para escribir código Python más rápido y eficiente, especialmente cuando se trabaja a gran escala.
Si quieres conocer otros artículos parecidos a Python __slots__: Guía Completa de Optimización puedes visitar la categoría Juegos.
