Introducci贸n
Contenido
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 clasename
ylocation
son variables de instancia (tenga en cuenta la palabra claveself
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 clasename
ylocation
son variables de instancia- La palabra clave
__slots__
es una variable de clase que contiene la lista de variables de instancia (name
ylocation
)
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 unaAttributeError
:
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.