Внешне это довольно просто: методы, переменные класса и строка документации класса хранятся в классе (строки функции хранятся в функции). Переменные экземпляра хранятся в экземпляре. Экземпляр также ссылается на класс, чтобы вы могли искать методы. Как правило, все они хранятся в словарях (__dict__
).
Так что да, краткий ответ: Python не хранит методы в экземплярах, но все экземпляры должны иметь ссылку на класс.
Например, если у вас есть простой класс, подобный этому:
class MyClass:
def __init__(self):
self.a = 1
self.b = 2
def __repr__(self):
return f"{self.__class__.__name__}({self.a}, {self.b})"
instance_1 = MyClass()
instance_2 = MyClass()
Тогда в памяти это выглядит (очень упрощенно) так:
Идя глубже
Однако есть несколько вещей, которые важны при углублении в CPython:
- Наличие словаря в качестве абстракции приводит к небольшим накладным расходам: вам нужна ссылка на словарь экземпляров (байты), и каждая запись в словаре хранит хеш (8 байт), указатель на ключ (8 байт) и указатель на сохраненный атрибут (еще 8 байтов). Кроме того, словари обычно перераспределяют, так что добавление другого атрибута не вызывает изменение размера словаря.
- Python не имеет "типов значений", даже целое число будет экземпляром. Это означает, что вам не нужно 4 байта для хранения целого числа - Python требует (на моем компьютере) 24 байта для хранения целого числа 0 и не менее 28 байтов для хранения целых чисел, отличных от нуля. Однако ссылки на другие объекты просто требуют 8 байтов (указатель).
- CPython использует подсчет ссылок, поэтому каждому экземпляру необходим счетчик ссылок (8 байт). Также большинство классов CPythons участвуют в циклическом сборщике мусора, который влечет за собой дополнительные 24 байта на экземпляр. В дополнение к этим классам, которые могут иметь слабые ссылки (большинство из них), также имеется поле
__weakref__
(еще 8 байтов).
На этом этапе также необходимо указать, что CPython оптимизирует некоторые из этих «проблем»:
- Python использует Словари совместного использования ключей , чтобы избежать некоторых накладных расходов памяти (хэш и ключ) словарей экземпляров.
- Вы можете использовать
__slots__
в классах, чтобы избежать __dict__
и __weakref__
. Это может дать значительно меньший объем памяти на экземпляр.
- Python интернирует некоторые значения, например, если вы создадите маленькое целое число, он не создаст новый целочисленный экземпляр, а вернет ссылку на уже существующий экземпляр.
Учитывая все это и то, что некоторые из этих моментов (особенно пункты об оптимизации) являются деталями реализации, трудно дать канонический ответ об эффективных требованиях к памяти для классов Python.
Сокращение объема памяти экземпляров
Однако, если вы хотите уменьшить объем памяти ваших экземпляров, обязательно попробуйте __slots__
. У них есть недостатки, но если они к вам не относятся, это очень хороший способ уменьшить память.
class Slotted:
__slots__ = ('a', 'b')
def __init__(self):
self.a = 1
self.b = 1
Если этого недостаточно, и вы работаете с большим количеством «типов значений», вы также можете пойти дальше и создать классы расширений. Это классы, которые определены в C, но упакованы так, что вы можете использовать их в Python.
Для удобства я использую привязки IPython для Cython, чтобы симулировать класс расширения:
%load_ext cython
%%cython
cdef class Extensioned:
cdef long long a
cdef long long b
def __init__(self):
self.a = 1
self.b = 1
Измерение использования памяти
Остается интересный вопрос после всей этой теории: как мы можем измерить память?
Я также использую обычный класс:
class Dicted:
def __init__(self):
self.a = 1
self.b = 1
Я обычно использую psutil
(даже если это прокси-метод) для измерения воздействия на память и просто измеряю, сколько памяти она использовала до и после. Измерения немного сдвинуты, потому что мне нужно как-то сохранить экземпляры в памяти, иначе память будет восстановлена (немедленно). Кроме того, это только приблизительное значение, потому что Python на самом деле занимает немного памяти, особенно при большом количестве операций создания / удаления.
import os
import psutil
process = psutil.Process(os.getpid())
runs = 10
instances = 100_000
memory_dicted = [0] * runs
memory_slotted = [0] * runs
memory_extensioned = [0] * runs
for run_index in range(runs):
for store, cls in [(memory_dicted, Dicted), (memory_slotted, Slotted), (memory_extensioned, Extensioned)]:
before = process.memory_info().rss
l = [cls() for _ in range(instances)]
store[run_index] = process.memory_info().rss - before
l.clear() # reclaim memory for instances immediately
Память не будет точно одинаковой для каждого запуска, потому что Python повторно использует некоторую память, а иногда также сохраняет память для других целей, но это должно по крайней мере дать разумную подсказку:
>>> min(memory_dicted) / 1024**2, min(memory_slotted) / 1024**2, min(memory_extensioned) / 1024**2
(15.625, 5.3359375, 2.7265625)
Я использовал min
здесь в основном потому, что мне было интересно, каков был минимум, и я разделил на 1024**2
, чтобы преобразовать байты в мегабайты.
Резюме: Как и ожидалось, нормальному классу с dict потребуется больше памяти, чем классам со слотами, но классы расширения (если применимо и доступны) могут иметь еще меньший объем памяти.
Еще одним инструментом, который может быть очень удобен для измерения использования памяти, является memory_profiler
, хотя я давно его не использовал.