Как работают классы в Python

В этой статье рассмотрим разные аспекты устройства классов в Python.
О метаклассах не упоминается намеренно, им место в другой статье.

Что такое тип данных

Тип — это объект. Когда выполняется программа, в памяти живут различные значения: числа, строки, словари и так далее. Где-то рядом с ними живёт тип int. Это отдельный кусок памяти, на который ссылаются переменные. Так же как и функции, типы — это first class citizen: они живут в памяти, их можно передавать как аргументы и проводить интроспекцию. Если создать класс, то количество объектов в памяти увеличится:

Тип — это инстанс типа type. Есть целое число (например, 28).
Это — инстанс типа int. А сам тип int — это инстанс типа type:

Тип и класс — это одно и то же. Во втором Python были old-style классы,
у них это было не так. С тех пор утекло много воды и теперь тип и класс — это одно и то же. Вот так в этом можно убедиться:

Раз класс — это инстанс type, то мы можем создать класс, создав
инстанс типа type. Вот так:

Это аналог привычного синтаксиса создания класса:

Функция type умеет не только определять тип объекта, но и создавать
новые типы. Для этого ей нужно передать три аргумента: название типа, базовые типы и атрибуты.

А ещё все типы наследуются от object, даже type. Это можно проверить так:

Важно не путать два вида отношений: инстанцирование и наследование. В примерах выше StringableFloat — наследник float и они оба — инстансы type.

Раз создание класса — это такое же инстацирование, как и создание переменной, то этим управляет один и тот же кусок кода — функция type. Давайте посмотрим, что происходит в этот момент, подробнее.

Как происходит инстанцирование

Заглянем в исходный код вызова type. Если коротко, то в нём вызывается tp_new и tp_init.

tp_new — это конструктор в привычном понимании. Под капотом его реализация (PyType_GenericNew) вызывает tp_alloc, который выделяет память, создаёт объект и сдаёт его сборщику мусора для отслеживания.

tp_init вызывает __init__, если выполнены необходимые условия:
передан правильный объект, есть память и всё такое.

Тут важно, что вызовом __init__ занимается type, его не нужно
напрямую вызывать в __new__.

__init__ вызывается только если __new__ вернул инстанс нужного класса. Если он возвращает что-то другое, __init__ не вызывается:

Исходя из всего этого лучший совет по использованию __new__ — не использовать его. На практике почти всегда нужно сделать что-то уже с созданным объектом, которому выделена память — для этого есть __init__.

__dict__

Переменные инстанса хранятся внутри инстанса. По сути инстанс и представляет из себя набор этих данных — всё остальное живёт в типе.

Самый распространённый метод хранения данных инстанса — переменная __dict__. Это словарь, который живёт под капотом инстанса класса: ключи — строки с названиями полей, значения — значения соответсвующих переменных. Пример:

Обратите внимание на поведение class_var: она появилась в __dict__ только после того, как была определена у инстанса класса. До этого её в __dict__ не было, хотя она была доступна через a.class_var (и вернула бы единицу).

__dict__ — это не представление данных инстанса в формате словаря, они именно так и хранятся. Это полноценный словарь:

Это значит, что под капотом у него могут происходить хеширование, коллизии, увеличение адресного пространства и реаллокация — как у обычного словаря. Также это позволяет нам делать то же, что с обычным словарём, например удалять элементы. Вернёмся к предыдущему примеру:

У самого типа A тоже есть __dict__ — ведь он инстанс type:

У классов __dict__ — это не словарь, а mappingproxy. Он делает несколько полезных вещей: во-первых, убеждается, что ключи словаря — строки, а во-вторых делает его доступным только для чтения. Действительно, добавить элемент в такой словарь не выйдет:

А ещё это значит, что можно в __dict__ инстанса добавить ключ не-строку:

Зачем нужна такая возможность — непонятно, но у классов её нет.
Это сделано для большей стабильности работы интерпретатора и возможности использовать оптимизации — для них открывается больше простора, если __dict__ у класса доступен только для чтения. Больше можно почитать в баге Bypassing __dict__ readonlyness на python.org (там 2007 год, но в общем всё актуально).

Переменной __dict__ нет в двух случаях: если класс реализован на C
или если у класса объявлены слоты. К первому случаю относятся почти все встроенные типы, а о втором поговорим подробнее.

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

Посмотреть, что происходит при использовании слотов можно в функции type_new. Если коротко, то над слотами проводятся проверки на их вменяемость, правильно считаются все ссылки для сборщика мусора и, наконец, создаётся список со значениями для инстанса.

Для демонстрации посмотрим, сколько памяти экономит использование слотов в простом случае. Чтобы посчитать полный размер объекта со всеми вложенными в него объектами (например, вместе с __dict__ и его ключами и значениями), используем модуль pympler:

Как работает доступ к атрибутам

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

Дескрипторы

Дескрипторы — объекты, которые умеют выполнять произвольный код, когда с ними происходят какие-то действия: доступ, изменение или удаление. Пример:

Обратите внимание, что после попытки удаления u.email он никуда не делся, просто был вызван метод __delete__ дескриптора.

Дескриптор определяет методы, которые вызываются в момент, когда происходит доступ (или удаление или редактирование) инстанса дескриптора как атрибута другого класса.

Всё это работает благодаря методу __getattribute__: он находит нужный атрибут и проверяет, есть ли у него метод __get__. Если есть — вызывает его, если нет — возвращает сам атрибут.

Именно через механизм дескрипторов осуществляется доступ к атрибутам класса и инстанса. classmethod, staticmethod, property и super тоже работают благодаря дескрипторам.

Порядок поиска атрибутов

Из одного из предыдущих примеров мы узнали, что при доступе к атрибуту инстанса, сначала происходит его поиск в __dict__ инстанса, а потом — в __dict__ класса. Но это с переменными. Теперь давайте разбираться, что происходит с методами. Начнём с примера:

Ага, метод лежит в __dict__ класса. Получается, что когда мы говорим a.method, происходит type(a).__dict__['method']? Давайте проверим:

Получается, что в __dict__ лежит простая функция, а когда мы делаем a.method — это уже атрибут. Что превращает одно в другое?

Как раз это и делается дескриптором: каждый метод оборачивается в него. Таким образом, type(a).__dict__['method'] — дескриптор, а a.method– это уже результат его работы. Проверим нашу догадку:

Получается, что a.method под капотом превращается в type(a).__dict__['method'].__get__(a, None).

А что будет, если мы будем искать метод класса? У него ведь нет инстанса, есть только класс. За это отвечает декоратор classmethod и это — дескриптор, который при доступе прокидывает в первый аргумент функции класс, а не объект. Похожим образом действуют staticmethod и property.

super — это тоже дескриптор. При своём вызове он отправляется в __mro__ типа объекта и вызывает дескриптор нужного метода у подходящего класса. Если нужны детали реализации, их
можно посмотреть в функции super_getattro.

Превращение a.method в type(a).__dict__['method'].__get__(a, None) происходит внутри метода __getattribute__. Если его неправильно перегрузить — всё может сломаться наихудшим образом. Трогать __getattribute__ — почти всегда плохая идея.

Наследование и MRO

До этого у нас был только один класс. А что, если их несколько и какие-то методы перегружены? Кто, когда и как догадается, метод какого именно класса нужно вызывать?

Структура наследования может быть сложной и запутанной. Её линеаризацией занимается алгоритм C3 внутри Python. В результате работы у классов появляется __mro__ — тупл из самого класса
и всех его родителей в том порядке, в котором нужно искать в них методы.

__getattribute__ остаётся только воспользоваться этим туплом и отыскать первый класс, в котором найдётся нужный метод. Этот код реализован на C, но на Python он бы выглядел примерно так:

Что не так с __del__

Напоследок давайте поговорим о __del__, с ним тоже всё не так просто.

Правильное название для __del__ — финализатор, а не деструктор.
Деструктор уничтожает объект, а этим занимается не __del__, а сборщик мусора. __del__ вызывается перед этим. Иногда.

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

__del__ не вызывается при наличии циклических зависимостей. Поиском и удалением таких объектов занимается циклический сборщик мусора, он не вызывает __del__. А вот начиная с Python 3.4 вызывается даже для циклических зависимостей, подробнее можно почитать в PEP-442. Можно избегать циклических зависимостей с помощью weakref, но чем больше проект — тем сложнее это делать.

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

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

Проблемы выше позволяют сформировать правило: не используйте __del__. Если вам нужен код, который в любом случае будет вызван, используйте with. Другой вариант — явно его вызвать с помощью try…finally. Тогда поведение финализатора будет предсказуемым, а код — более поддерживаемым.