Использование __slots__ для хранения объектных данных в Python

Введение В Python каждый экземпляр объекта поставляется со стандартными функциями и атрибутами. Например, Python использует словарь для хранения атрибутов экземпляра объекта. Это дает много преимуществ, например, позволяет нам добавлять новые атрибуты во время выполнения. Однако за это удобство приходится платить. Словари могут потреблять значительную часть памяти, особенно если у нас есть много экземпляров объектов с большим количеством атрибутов. Если производительность и эффективность использования памяти критичны, мы не можем

Вступление

В Python каждый экземпляр объекта поставляется со стандартными функциями и атрибутами. Например, Python использует словарь для хранения атрибутов экземпляра объекта. Это дает много преимуществ, например, позволяет нам добавлять новые атрибуты во время выполнения. Однако за это удобство приходится платить.

Словари могут потреблять значительную часть памяти, особенно если у нас есть много экземпляров объектов с большим количеством атрибутов. Если производительность и эффективность использования памяти кода критичны, мы можем обменять удобство словарей на __slots__ .

В этом руководстве мы рассмотрим, что такое __slots__ и как их использовать в Python. Мы также обсудим компромиссы при использовании __slots__ и посмотрим на их производительность по сравнению с типичными классами, которые хранят свои атрибуты экземпляров в словарях.

Что такое _ слоты_ и как их использовать?

Слоты - это переменные класса, которым можно назначить строку, итерацию или последовательность строк имен переменных экземпляра. При использовании слотов вы заранее называете переменные экземпляра объекта, теряя возможность добавлять их динамически.

Экземпляр объекта, использующий слоты, не имеет встроенного словаря. В результате экономится больше места, а доступ к атрибутам осуществляется быстрее.

Посмотрим на это в действии. Рассмотрим этот обычный класс:

 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 

В приведенном выше фрагменте:

  • organization - это переменная класса
  • name и location являются переменными экземпляра (обратите внимание на ключевое слово self перед ними)

При создании каждого экземпляра объекта класса динамический словарь выделяется под именем атрибута как __dict__ который включает все доступные для записи атрибуты объекта. Результатом приведенного выше фрагмента кода является:

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

Графически это можно представить как:

Рисунок 1: Поведение объекта нормальногокласса{.ezlazyload}

Теперь давайте посмотрим, как мы можем реализовать этот класс с помощью слотов:

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

В приведенном выше фрагменте:

  • organization - это переменная класса
  • name и location - переменные экземпляра
  • Ключевое слово __slots__ - это переменная класса, содержащая список переменных экземпляра ( name и location ).

Запуск этого кода даст нам эту ошибку:

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

Верно! Экземпляры объектов классов со слотами не имеют атрибута __dict__ За кулисами, вместо того, чтобы хранить переменные экземпляра в словаре, значения сопоставляются с местоположениями индексов, как показано на рисунке ниже:

Рисунок 2: Поведение объекта классас{.ezlazyload}

Хотя __dict__ нет, вы по-прежнему получаете доступ к свойствам объекта, как обычно:

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

Слоты были созданы исключительно для повышения производительности, как заявил Гвидо в своем авторитетном блоге .

Посмотрим, превзойдут ли они стандартные классы.

Эффективность и скорость слотов

Мы собираемся сравнить объекты, созданные с помощью слотов, с объектами, созданными с помощью словарей, с двумя тестами. В нашем первом тесте мы рассмотрим, как они распределяют память. Наш второй тест будет смотреть на их время выполнения.

Этот сравнительный анализ памяти и времени выполнения выполняется на Python 3.8.5 с использованием модулей tracemalloc для отслеживания распределения памяти и timeit для оценки времени выполнения.

Результаты могут отличаться на вашем персональном компьютере:

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

В приведенном выше фрагменте кода calculate_memory() определяет выделенную память, а calculate_runtime() определяет оценку времени выполнения класса со слотами по сравнению с классом без слотов.

Результаты будут выглядеть примерно так:

 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 

Очевидно, что использование __slots__ дает преимущество перед словарями по размеру и скорости. Хотя разница в скорости не особо заметна, разница в размерах значительна.

Попытки использовать слоты

Прежде чем вы начнете использовать слоты во всех своих классах, следует помнить о нескольких предостережениях:

  1. Он может хранить только атрибуты, определенные в переменной класса __slots__ Например, в следующем фрагменте, когда мы пытаемся установить атрибут для экземпляра, которого нет в __slots__ , мы получаем AttributeError :
1
<!-- -->
 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" 

Выход:

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

Для слотов вам необходимо знать все атрибуты, присутствующие в классе, и определить их в переменной __slots__

  1. __slots__ не будут следовать назначению __slots__ в суперклассе. Допустим, вашему базовому классу __slots__ который наследуется подклассу, подкласс по умолчанию __dict__

Рассмотрим следующий фрагмент, в котором объект подкласса проверяется, если его каталог содержит __dict__ и результат оказывается 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)) 

Выход:

 True 

Этого можно __slots__ еще раз объявив переменную __slots__ для подкласса для всех переменных экземпляра, присутствующих в подклассе. Хотя это кажется избыточным, усилия можно сопоставить с объемом сохраненной памяти:

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

Выход:

 False 

Заключение

В этой статье мы узнали основы __slots__ и то, чем классы со слотами отличаются от классов со словарями. Мы также протестировали эти два класса со слотами, значительно более эффективными с точки зрения памяти. Наконец, мы обсудили некоторые известные предостережения при использовании слотов в классах.

При использовании в правильных местах __slots__ может повысить производительность и оптимизировать код, сделав его более эффективным с точки зрения памяти.

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus