Вступление
В 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'}
Графически это можно представить как:
{.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__
За кулисами, вместо того, чтобы хранить переменные экземпляра
в словаре, значения сопоставляются с местоположениями индексов, как
показано на рисунке ниже:
{.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__
дает преимущество перед
словарями по размеру и скорости. Хотя разница в скорости не особо
заметна, разница в размерах значительна.
Попытки использовать слоты
Прежде чем вы начнете использовать слоты во всех своих классах, следует помнить о нескольких предостережениях:
- Он может хранить только атрибуты, определенные в переменной класса
__slots__
Например, в следующем фрагменте, когда мы пытаемся установить атрибут для экземпляра, которого нет в__slots__
, мы получаем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"
Выход:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'character_with_slots' object has no attribute 'pet'
Для слотов вам необходимо знать все атрибуты, присутствующие в классе, и
определить их в переменной __slots__
__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__
может повысить
производительность и оптимизировать код, сделав его более эффективным с
точки зрения памяти.