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 *