Когда выделяестя память для методов экземпляра класса: при его создании, или при обращении к данному методу?

Рейтинг: 1Ответов: 2Опубликовано: 15.04.2023

Допустим, имеется класс, экземпляр которого должен иметь относительно немного атрибутов (переменных) и пару десятков методов на несколько десятков килобайт для работы с ними. И предполагается, что таких экземпляров (объектов) будет довольно много. Первое, что приходит на ум, это то, что конструктор класса при создании каждого экземпляра выделит память для всех методов и будет хранить их код на протяжении жизни объекта. Но логичнее было бы, если память под код конкретного метода выделялась бы в момент обращения к нему и освобождалась по окончании исполнения этого кода. Если верно первое, то придется по возможности использовать статические методы вместо методов экземпляра, а если второе, то можно создавать методы экземпляра класса без ограничений. Хочется узнать мнение экспертов об этом.

Ответы

▲ 1

Если бы методы класса дублировались в каждом экземпляре, то для списка из 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']
▲ 0

Ещё поразмыслив, я всё же склонился к тому, что код методов экземпляров класса размещается в памяти динамически: Во-первых, методы init и del совершенно бессмысленно хранить в памяти для каждого экземпляра: они выполняются всего один раз (а синтаксически - это однозначно методы экземпляра класса). Во-вторых, в любой момент к существующим методам экземпляров класса можно добавить новый:

    def newmethod(self):
        pass
    Myclass.meth99=newmethod

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