Когда выделяестя память для методов экземпляра класса: при его создании, или при обращении к данному методу?
Допустим, имеется класс, экземпляр которого должен иметь относительно немного атрибутов (переменных) и пару десятков методов на несколько десятков килобайт для работы с ними. И предполагается, что таких экземпляров (объектов) будет довольно много. Первое, что приходит на ум, это то, что конструктор класса при создании каждого экземпляра выделит память для всех методов и будет хранить их код на протяжении жизни объекта. Но логичнее было бы, если память под код конкретного метода выделялась бы в момент обращения к нему и освобождалась по окончании исполнения этого кода. Если верно первое, то придется по возможности использовать статические методы вместо методов экземпляра, а если второе, то можно создавать методы экземпляра класса без ограничений. Хочется узнать мнение экспертов об этом.
Ответы (2 шт):
Ещё поразмыслив, я всё же склонился к тому, что код методов экземпляров класса размещается в памяти динамически: Во-первых, методы init и del совершенно бессмысленно хранить в памяти для каждого экземпляра: они выполняются всего один раз (а синтаксически - это однозначно методы экземпляра класса). Во-вторых, в любой момент к существующим методам экземпляров класса можно добавить новый:
def newmethod(self):
pass
Myclass.meth99=newmethod
Если бы у каждого уже существующего экземпляра класса присутствовала в памяти программы копия кода всех методов экземпляра класса, то добавление нового метода было бы довольно сложной и затратной задачей.
Если бы методы класса дублировались в каждом экземпляре, то для списка из 1000 000 int'ов было бы по 1 млн копий каждого метода положенного типу int, на деле же каждый экземпляр имеет ссылку на тип/класс int, через которую получает доступ к общим методам.
В Python всё является объектом, в том числе создаваемые пользователем классы/типы и функции создаваемые с помощью def/lambda. Функции написанные в коде тела класса называются методами и представляют собой функции оборачиваемые на лету в дополнительный объект типа Method (условное название). Принадлежат они объекту класса, а не его экземплярам.
Объект-обёртка позволяет при вызове метода неявно передавать первым аргументом объект у которого этот метод вызывается: self, cls. В случае обычного метода это сам экземпляр, обычно называемый self в сигнатуре метода, в случае classmethod поведение меняется декоратором @classmethod и первым аргументом передаётся объект класса, который принято называть cls. staticmethod сделан так, чтобы при обращении к методу, возвращалась сама функция, без оборачивания в объект типа Method.
Код функций написанных в юзерском классе на этапе компиляции превращается в code objects, затем на этапе исполнения отрабатывают конструкции def внутри класса и создаются function objects - заготовки под методы. Ссылками на эти function objects владеет объект-класс и добраться до них можно через его атрибуты, CPython хранит function objects в сегменте heap, как и все остальные объекты исполняемой программы. Одна функция внутри тела класса соответствует одному объекту в heap'е. Экземпляры же получают доступ к этому объекту через класс родитель. И для 100, и для 1000 экземпляров будет 1 function object в heap'e.
Пример:
Конструкция class MyClass в коде ведёт к созданию объекта MyClass. Имена в теле класса MyClass становятся его атрибутами. При создании экземпляра obj = MyClass() новому объекту obj записывается ссылка на MyClass. При вызове у obj метода - obj.class_body_method(some_arg), происходит приблизительно следующее:
поиск атрибута
class_body_methodв определённом порядке - у самого экземпляраobj, в цепочке mro его родителя -MyClass, в метаклассе - типе от которого происходитMyClass(обычно этоtype). В экземпляре ссылка на метод может храниться только если он был целенаправленно сохранён в качестве атрибута экземпляра.так как методы класса реализованы при помощи descriptor protocol, после нахождении атрибута/дескриптора
class_body_method, у него вызывается метод__get__, в котором происходят манипуляции, необходимые для превращения функции в метод определённого типа. В связи с тем, что превращение функции в обычный метод происходит при каждом обращении, id'шники вновь создаваемого метод-объекта будут разные, но не дляstaticmethod, так как там оборачивания не происходит и возвращается сама функция, каждый раз один и тот же объект, соответственно и id'шник не меняется.
Демонстрация различных моментов упомянутых в предыдущем тексте
class MyClass:
def class_body_method_1(self):
pass
def class_body_method_2(self, arg2, some_arg=[]):
some_arg.append(arg2)
print(some_arg)
@staticmethod
def class_body_staticmethod():
pass
obj1 = MyClass()
obj2 = MyClass()
obj3 = MyClass()
Смена id'шника при каждом обращении к обычному методу:
In []: id(obj1.class_body_method_1)
Out[]: 140714545102720
In []: id(obj1.class_body_method_1)
Out[]: 140714546939712
Но не в случае @staticmethod:
In []: id(obj1.class_body_staticmethod)
Out[]: 140714331142272
In []: id(obj1.class_body_staticmethod)
Out[]: 140714331142272
Потому что при обращении к обычному методу каждый раз заново создаётся и возвращается объект-метод:
In []: obj1.class_body_method_1
Out[]: <bound method MyClass.class_body_method_1 of <__main__.MyClass object at 0x7ffaa8a0c790>>
Имеющий ссылку на один и тот же объект-функцию:
In []: obj1.class_body_method_1.__func__
Out[]: <function __main__.MyClass.class_body_method_1(self)>
In []: id(obj1.class_body_method_1.__func__)
Out[]: 140714331143568
In []: id(obj1.class_body_method_1.__func__)
Out[]: 140714331143568
А staticmethod просто возвращает саму функцию:
In []: obj1.class_body_staticmethod
Out[]: <function __main__.MyClass.class_body_staticmethod()>
Следствием и доказательством одного функции-объекта для всех экземпляров являются, например, такие вещи (один дефолтный массив на всех):
In []: obj1.class_body_method_2("a")
['a']
In []: obj1.class_body_method_2("b")
['a', 'b']
In []: obj2.class_body_method_2("c")
['a', 'b', 'c']
In []: obj3.class_body_method_2("d")
['a', 'b', 'c', 'd']