Usando __slots__ para almacenar datos de objetos en Python

En este tutorial, aprenderá qué son las tragamonedas en Python. Discutiremos sus mejoras de memoria y velocidad, así como las ventajas y desventajas de usarlos.

Introducción

En Python, cada instancia de objeto viene prediseñada con funciones y atributos estándar. Por ejemplo, Python usa un diccionario para almacenar los atributos de instancia de un objeto. Esto tiene muchos beneficios, como permitirnos agregar nuevos atributos en tiempo de ejecución. Sin embargo, esta conveniencia tiene un costo.

Los diccionarios pueden consumir una buena cantidad de memoria, especialmente si tenemos muchos objetos de instancia con una gran cantidad de atributos. Si el rendimiento y la eficiencia de la memoria del código son críticos, podemos cambiar la comodidad de los diccionarios por __slots__.

En este tutorial, veremos qué son las __ranuras__ y cómo usarlas en Python. También discutiremos las ventajas y desventajas de usar __slots__, y veremos su rendimiento en comparación con las clases típicas que almacenan sus atributos de instancia con diccionarios.

¿Qué son _tragamonedas_ y cómo usarlas?

Las ranuras son variables de clase a las que se les puede asignar una cadena, un iterable o una secuencia de cadenas de nombres de variables de instancia. Cuando usa ranuras, nombra las variables de instancia de un objeto por adelantado, perdiendo la capacidad de agregarlas dinámicamente.

Una instancia de objeto que usa ranuras no tiene un diccionario integrado. Como resultado, se ahorra más espacio y el acceso a los atributos es más rápido.

Veámoslo en acción. Considere esta clase regular:

1
2
3
4
5
6
7
8
9
class CharacterWithoutSlots():
    organization = "Slate Rock and Gravel Company"

    def __init__(self, name, location):
        self.name = name
        self.location = location

without_slots = character_without_slots('Fred Flinstone', 'Bedrock')
print(without_slots.__dict__)  # Print the arguments

En el fragmento anterior:

  • organización es una variable de clase
  • nombre y ubicación son variables de instancia (tenga en cuenta la palabra clave auto delante de ellas)

Si bien se crea cada instancia de objeto de la clase, se asigna un diccionario dinámico bajo el nombre del atributo como __dict__ que incluye todos los atributos de escritura de un objeto. El resultado del fragmento de código anterior es:

1
{'name': 'Fred Flinstone', 'location': 'Bedrock'}

Esto se puede representar pictóricamente como:

Figure 1: Behavior of a Normal Class Object

Ahora, veamos cómo podemos implementar esta clase usando ranuras:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class CharacterWithSlots():
    __slots__ = ["name", "location"]
    organization = "Slate Rock and Gravel Company"

    def __init__(self, name, location):
        self.name = name
        self.location = location

with_slots = CharacterWithSlots('Fred Flinstone', 'Bedrock')
print(with_slots.__dict__)

En el fragmento anterior:

  • organización es una variable de clase
  • nombre y ubicación son variables de instancia
  • La palabra clave __slots__ es una variable de clase que contiene la lista de las variables de instancia (nombre y ubicación)

Ejecutar ese código nos dará este error:

1
2
3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'character_without_slots' object has no attribute '__dict__'

¡Así es! Las instancias de objetos de clases con ranuras no tienen un atributo __dict__. Detrás de escena, en lugar de almacenar las variables de instancia en un diccionario, los valores se asignan con las ubicaciones de índice como se muestra en la siguiente figura:

Figura 2: Comportamiento de un Objeto de Clase con

Si bien no hay un atributo __dict__, aún accede a las propiedades del objeto como lo haría normalmente:

1
2
3
print(with_slots.name)         # Fred Flinstone
print(with_slots.location)     # Bedrock
print(with_slots.organization) # Slate Rock and Gravel Company

Las tragamonedas se crearon exclusivamente para mejorar el rendimiento, como lo indica Guido en su publicación de blog autorizada .

Veamos si superan a las clases estándar.

Eficiencia y velocidad de las tragamonedas

Vamos a comparar objetos instanciados con ranuras con objetos instanciados con diccionarios con dos pruebas. Nuestra primera prueba analizará cómo asignan la memoria. Nuestra segunda prueba analizará sus tiempos de ejecución.

Esta evaluación comparativa de la memoria y el tiempo de ejecución se realiza en Python 3.8.5 utilizando los módulos tracemalloc para el seguimiento de la asignación de memoria y timeit para la evaluación del tiempo de ejecución.

Los resultados pueden variar en su computadora personal:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import tracemalloc
import timeit

# The following `Benchmark` class benchmarks the
# memory consumed by the objects with and without slots
class Benchmark:
    def __enter__(self):
        self.allocated_memory = None
        tracemalloc.start()
        return self

    def __exit__(self, exec_type, exec_value, exec_traceback):
        present, _ = tracemalloc.get_traced_memory()
        tracemalloc.stop()
        self.allocated_memory = present


# The class under evaluation. The following class
# has no slots initialized
class CharacterWithoutSlots():
    organization = "Slate Rock and Gravel Company"

    def __init__(self, name, location):
        self.name = name
        self.location = location


# The class under evaluation. The following class
# has slots initialized as a class variable
class CharacterWithSlots():
    __slots__ = ["name", "location"]
    organization = "Slate Rock and Gravel Company"

    def __init__(self, name, location):
        self.name = name
        self.location = location


# The following `calculate_memory` function creates the object for the
# evaluated `class_` argument corresponding to the class and finds the
# memory used
def calculate_memory(class_, number_of_times):
    with Benchmark() as b:
        _ = [class_("Barney", "Bedrock") for x in range(number_of_times)]
    return b.allocated_memory / (1024 * 1024)


# The following `calculate_runtime` function creates the object for the
# evaluated `class_` argument corresponding to the class and finds the
# runtime involved
def calculate_runtime(class_, number_of_times):
    timer = timeit.Timer("instance.name; instance.location",
                         setup="instance = class_('Barney', 'Bedrock')",
                         globals={'class_': class_})
    return timer.timeit(number=number_of_times)


if __name__ == "__main__":
    number_of_runs = 100000   # Alter the number of runs for the class here

    without_slots_bytes = calculate_memory(
        CharacterWithoutSlots, number_of_runs)
    print(f"Without slots Memory Usage: {without_slots_bytes} MiB")

    with_slots_bytes = calculate_memory(CharacterWithSlots, number_of_runs)
    print(f"With slots Memory Usage: {with_slots_bytes} MiB")

    without_slots_seconds = calculate_runtime(
        CharacterWithoutSlots, number_of_runs)
    print(f"Without slots Runtime: {without_slots_seconds} seconds")

    with_slots_seconds = calculate_runtime(
        CharacterWithSlots, number_of_runs)
    print(f"With slots Runtime: {with_slots_seconds} seconds")

En el fragmento anterior, la función calculate_memory() determina la memoria asignada, y la función calculate_runtime() determina la evaluación del tiempo de ejecución de la clase con ranuras frente a la clase sin ranuras.

Los resultados se verán algo a lo largo de estas líneas:

1
2
3
4
Without slots Memory Usage: 15.283058166503906 MiB
With slots Memory Usage: 5.3642578125 MiB
Without slots Runtime: 0.0068232000012358185 seconds
With slots Runtime: 0.006200600000738632 seconds

Es evidente que usar __slots__ da una ventaja sobre el uso de diccionarios en tamaño y velocidad. Si bien la diferencia de velocidad no es particularmente notable, la diferencia de tamaño es significativa.

Problemas con el uso de tragamonedas

Antes de comenzar a usar tragamonedas en todas sus clases, hay algunas advertencias que debe tener en cuenta:

  1. Solo puede almacenar atributos definidos en la variable de clase __slots__. Por ejemplo, en el siguiente fragmento, cuando intentamos establecer un atributo para una instancia que no está presente en la variable __slots__, obtenemos un AttributeError:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class character_with_slots():
    __slots__ = ["name", "location"]
    organization = "Slate Rock and Gravel Company"

    def __init__(self, name, location):
        self.name = name
        self.location = location

with_slots = character_with_slots('Fred Flinstone', 'Bedrock')
with_slots.pet = "dino"

Producción:

1
2
3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'character_with_slots' object has no attribute 'pet'

Con las ranuras, necesitas conocer todos los atributos presentes en la clase y definirlos en la variable __slots__.

  1. Las subclases no seguirán la asignación __slots__ en la superclase. Digamos que su clase base tiene asignado el atributo __slots__ y esto se hereda a una subclase, la subclase tendrá un atributo __dict__ por defecto.

Considere el siguiente fragmento donde el objeto de la subclase se verifica si su directorio contiene el atributo __dict__ y la salida resulta ser Verdadero:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class CharacterWithSlots():
    __slots__ = ["name", "location"]
    organization = "Slate Rock and Gravel Company"

    def __init__(self, name, location):
        self.name = name
        self.location = location


class SubCharacterWithSlots(CharacterWithSlots):
    def __init__(self, name, location):
        self.name = name
        self.location = location

sub_object = SubCharacterWithSlots("Barney", "Bedrock")

print('__dict__' in dir(sub_object))

Producción:

1
True

Esto se puede evitar declarando la variable __slots__ una vez más para la subclase para todas las variables de instancia presentes en la subclase. Aunque esto parece redundante, el esfuerzo puede compararse con la cantidad de memoria ahorrada:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class CharacterWithSlots():
    __slots__ = ["name", "location"]
    organization = "Slate Rock and Gravel Company"

    def __init__(self, name, location):
        self.name = name
        self.location = location

class SubCharacterWithSlots(CharacterWithSlots):
    __slots__ = ["name", "location", "age"]

    def __init__(self, name, location):
        self.name = name
        self.location = location
        self.age = 40

sub_object = SubCharacterWithSlots("Barney", "Bedrock")

print('__dict__' in dir(sub_object))

Producción:

1
False

Conclusión

En este artículo, hemos aprendido los conceptos básicos sobre el atributo __slots__ y cómo las clases con espacios difieren de las clases con diccionarios. También comparamos esas dos clases con ranuras que son significativamente más eficientes con la memoria. Finalmente, discutimos algunas advertencias conocidas con el uso de espacios en las clases.

Si se usa en los lugares correctos, __slots__ puede aumentar el rendimiento y optimizar el código para que sea más eficiente en memoria. .

Licensed under CC BY-NC-SA 4.0