En Python, los atributos de instancia se almacenan por defecto en un diccionario llamado __dict__. Los diccionarios consumen una cantidad significativa de memoria, a pesar de ciertas optimizaciones internas. Sin embargo, si definimos un atributo de clase llamado __slots__ que contiene una secuencia de nombres de atributos, Python utilizará un modelo alternativo para almacenar los atributos de instancia: los nombres especificados en __slots__ se guardan en un array oculto que consume menos memoria que un diccionario. A continuación, veamos algunos ejemplos ilustrativos.
>>> class Punto:
... __slots__ = ('coord_x', 'coord_y') # 1
...
>>> p = Punto() # 2
>>> p.__dict__ # 3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Punto' object has no attribute '__dict__'. Did you mean: '__dir__'?
>>> p.coord_x = 10 # 4
>>> p.coord_y = 20
>>> p.etiqueta = 'importante' # 5
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Punto' object has no attribute 'etiqueta'
__slots__debe declararse durante la definición de la clase; cualquier intento de agregarlo o modificarlo después será ineficaz. Los nombres de los atributos pueden almacenarse en una tupla o lista, aunque se recomienda usar tuplas ya que esto indica explícitamente que__slots__no puede modificarse.- Se crea una instancia de Punto para demostrar los efectos de
__slots__. - Primer efecto: las instancias de Punto no tienen el atributo
__dict__. - Se asignan normalmente los atributos p.coord_x y p.coord_y.
- Segundo efecto: intentar asignar un atributo no incluido en
__slots__genera un AttributeError.
Si definimos una subclase de Punto, podemos observar un comportamiento contraintuitivo de __slots__.
>>> class PuntoFlexible(Punto): # 1
... pass
...
>>> pf = PuntoFlexible()
>>> pf.__dict__ # 2
{}
>>> pf.coord_x = 8 # 3
>>> pf.__dict__ # 4
{}
>>> pf.coord_x # 5
8
>>> pf.etiqueta = 'verde' # 6
>>> pf.__dict__ # 7
{'etiqueta': 'verde'}
- PuntoFlexible no declara ningún atributo propio.
- Sucede algo inesperado: las instancias de PuntoFlexible sí tienen el atirbuto
__dict__. - Aunque se asigne el atributo coord_x (definido en los
__slots__de la clase base Punto). - Este no se almacena en el atributo
__dict__de la enstancia. - Sino que se guarda en un array oculto de refreencias de la instancia.
- Al asignar un atributo no incluido en
__slots__. - Este se almacena en el atributo
__dict__de la instancia.
El ejemplo anterior demuestra que las subclases solo heredan parcialmente los efectos de __slots__. Para garantizar que las instancias de la subclase tampoco tengan el atributo __dict__, es necesario volver a declarar el atributo __slots__ en la subclase.
Si en la subclase declaramos __slots__ = () (una tupla vacía), las instancias de la subclase no tendrán el atributo __dict__ y solo aceptarán los nombres de atributos listados en los __slots__ de la clase base.
Si la subclase necesita atributos adicionales, estos deben listarse en el atributo __slots__ de la subclase.
>>> class PuntoColorido(Punto):
... __slots__ = ('color',) # 1
...
>>> pc = PuntoColorido()
>>> pc.__dict__ # 2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'PuntoColorido' object has no attribute '__dict__'. Did you mean: '__dir__'?
>>> pc.coord_x = 2
>>> pc.color = 'azul' # 3
>>> pc.sabor = 'plátano'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'PuntoColorido' object has no attribute 'sabor'
- Los
__slots__de la superclase se añaden automáticamente a los__slots__de la clase actual. - Las instancias de PuntoColorido no tienen el atributo
__dict__. - Es posible asignar atributos declarados en los
__slots__de la clase actual y la superclase, pero no otros atributos.
Sin embargo, el ahorro de memoria puede verse comprometido: si añadimos el nombre __dict__ a la lista de __slots__, las instancias almacenarán los nombres de __slots__ en un array de referencias exclusivo de cada instancia, pero también permitirán la creación dinámica de atributos, que se almacenarán en el __dict__ convencional. Por supuesto, añadir __dict__ a __slots__ puede contradecir el propósito original, dependiendo de la cantidad y el uso de atributos estáticos y dinámicos en cada instancia. Una optimización descuidada puede ser incluso peor que una optimización prematura, y a menudo resulta contraproducente.
Consideraciones importantes:
Si se utiliza correctamente, el atributo de clase __slots__ puede reducir significativamente el consumo de memoria, pero hay varios aspectos a tener en cuenta:
- Cada subclase debe volver a declarar el atributo
__slots__para evitar que sus instancias tengan el atributo__dict__. - Las instancias solo pueden tener los atributos listados en
__slots__, a menos que se añada__dict__a__slots__(pero esto anularía el ahorro de memoria). - Las clases con
__slots__no pueden utilizar el decorador @cached_property a menos que se añada__dict__a__slots__. - Si no se incluye
__weakref__en__slots__, las instancias no pueden ser objetivo de referencias débiles.