Usando __slots__ para almacenar datos de objetos en Python

    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 parte de la 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 conveniencia de los diccionarios por __slots__.

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

    ¿Qué son los _slots_ y cómo utilizarlos?

    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 incorporado. 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:

    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 de arriba:

    • organization es una variable de clase
    • name y location son variables de instancia (tenga en cuenta la palabra clave self en frente de ellos)

    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:

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

    Esto se puede representar gráficamente como:

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

    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 de arriba:

    • organization es una variable de clase
    • name y location son variables de instancia
    • La palabra clave __slots__ es una variable de clase que contiene la lista de variables de instancia (name y location)

    Ejecutar ese código nos dará este error:

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

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

    Mientras no haya __dict__ atributo, todavía accede a las propiedades del objeto como lo haría normalmente:

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

    Las tragamonedas se crearon únicamente 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 ranuras

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

    Esta evaluación comparativa de memoria y 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:

    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, el calculate_memory() La función determina la memoria asignada y la calculate_runtime() La función determina la evaluación en tiempo de ejecución de la clase con ranuras frente a la clase sin ranuras.

    Los resultados se verán algo así:

    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 las tragamonedas

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

    • Solo puede almacenar atributos definidos en el __slots__ variable de clase. Por ejemplo, en el siguiente fragmento, cuando intentamos establecer un atributo para una instancia que no está presente en el __slots__ variable, obtenemos una AttributeError:
    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"
    

    Salida:

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

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

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

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

    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))
    

    Salida:

    True
    

    Esto se puede evitar declarando el __slots__ variable 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 guardada:

    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))
    

    Salida:

    False
    

    Conclusión

    En este artículo, hemos aprendido los conceptos básicos sobre __slots__ atributo, y cómo las clases con ranuras difieren de las clases con diccionarios. También comparamos esas dos clases con ranuras que son significativamente más eficientes en memoria. Finalmente, discutimos algunas advertencias conocidas sobre 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.

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *