Usando __slots__ para almacenar datos de objetos en Python

U

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.

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias para su correcto funcionamiento. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad