Optimización de memoria en Python con __slots__

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'

  1. __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.
  2. Se crea una instancia de Punto para demostrar los efectos de __slots__.
  3. Primer efecto: las instancias de Punto no tienen el atributo __dict__.
  4. Se asignan normalmente los atributos p.coord_x y p.coord_y.
  5. 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'}

  1. PuntoFlexible no declara ningún atributo propio.
  2. Sucede algo inesperado: las instancias de PuntoFlexible sí tienen el atirbuto __dict__.
  3. Aunque se asigne el atributo coord_x (definido en los __slots__ de la clase base Punto).
  4. Este no se almacena en el atributo __dict__ de la enstancia.
  5. Sino que se guarda en un array oculto de refreencias de la instancia.
  6. Al asignar un atributo no incluido en __slots__.
  7. 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'

  1. Los __slots__ de la superclase se añaden automáticamente a los __slots__ de la clase actual.
  2. Las instancias de PuntoColorido no tienen el atributo __dict__.
  3. 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.

Etiquetas: Python optimización memoria __slots__ atributos

Publicado el 9-13 16:53