Найти в Дзене
Future People

__init__ или __new__?Метакласс тут при чём?

Оглавление

Метаклассы - магия?

В комментариях вспомнили одну интригующую фразу Тима Питерса:

"Метаклассы это глубокая магия, о которой 99% пользователей даже не нужно задумываться. Если вы думаете, нужно ли вам их использовать — вам не нужно (люди, которым они реально нужны, точно знают, зачем они им, и не нуждаются в объяснениях, почему)."

Но неужели нам не хочется научиться магии?

Или всё сложно? А давайте-ка попробуем 😁

Создаём экземпляр класса привычным способом

Определим простой класс:

-2

При создании экземпляра MyClass кажется, что первым делом вызывается метод __new__ (конструктор), затем — __init__ (инициализатор). Но если копнуть глубже, то оказывается, что процесс создания экземпляра контролируется метаклассом. Разберёмся по шагам.

Базовый пример

-3

Результат:

-4

На первый взгляд:

1. Сначала вызывается __new__, который создаёт объект и возвращает его.
2. Потом вызывается __init__, который инициализирует созданный __new__ объект.

Но это упрощённая картина. На самом деле первым делом в игру вступает метакласс.

Роль метакласса

Все классы в Python являются экземплярами метакласса (по умолчанию это type).

Когда мы пишем `x = MyClass(10)`, Python вызывает метод __call__ у метакласса класса MyClass.

Давайте убедимся в этом:

-5

Вывод:

-6

Теперь мы уверены:

1. Сначала вызывается Meta.__call__.
2. Уже внутри него вызывается MyClass.__new__.
3. Если __new__ вернул экземпляр MyClass, вызывается MyClass.__init__.

Проверка с type

Может возникнуть вопрос:

«А вдруг вызов __call__ происходит только потому, что мы явно указали metaclass=Meta?»

Чтобы убедиться в обратном, рассмотрим обычный класс:

-7

Результат будет одинаковым в обоих случаях:

-8

То есть при любом создании объекта фактически вызывается __call__ у метакласса.
Отсюда следует несколько выводов:

1. Первым в цепочке действительно вызывается __call__ у метакласса.
2. Метод __new__ вызывается изнутри __call__.
3. Метод __init__ вызывается после успешного создания объекта в __new__.
4. Если __new__ вернёт не экземпляр класса, __init__ не вызовется.

Таким образом, __new__ и __init__ — это «верхний уровень» процесса инстанцирования, а реальный «дирижёр» — метакласс.

Самый интересный вопрос

А теперь давайте разберёмся, почему процесс инстанцирования (создания экземпляра класса) происходит именно так, как мы описали выше.

И для начала вспомним отличную фразу: "В Python всё - объекты".

Посмотрим внимательно на эту строку кода:

-9

Точнее, на её правую часть

-10

`MyClass` возвращает ссылку на сам объект - то есть на наш класс `MyClass`.

Затем следует вызов с помощью скобок и передача аргумента.

То есть скобки - это вызов __call__ у объекта `MyClass` (на первый взгляд, но это не совсем так, дальше разберём).

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

-11

Убедимся в этом:

-12

Получаем:

-13

Запомним этот момент и переключим внимание на другой пример.

Определим класс, экземпляры которого будут вызываемыми объектами.

Создадим экземпляр `MyCallableObj` и затем вызовем его:

-14

Получаем:

-15

Тут начинает приходит понимание))

`obj` - объект, как и всё в Python.
В парадигме ООП: `obj` - экземпляр класса `MyCallableObj`.
Когда мы вызываем `obj`:

-16

то на самом деле происходит следующее:

-17

То есть в этот момент мы получаем тип `obj` (то есть его класс `MyCallableObj`) и вызываем у полученного класса метод __call__ , в который передаём ссылку на сам экземпляр `obj`.

Давайте проверим:

-18

Получаем:

-19

Фиксируем следующее наблюдение:

`obj` - экземпляр класса `MyCallableObj`.

Значит при вызове экземпляра `obj` метод `__call__` будет вызываться у его типа (класса `MyCallableObj`).

Теперь возвращаемся к примеру с метаклассом:

-20

Здесь для создания экземпляра `MyClass` мы вызываем сам класс `MyClass`.

Снова вспоминаем "В Python - всё объекты".
В парадигме ООП: `MyClass` - тоже чей-то экземпляр.
Конкретно в этом случае `MyClass` - экземпляр метакласса `type`.
Это определено в Python по умолчанию: для всех классов классом-создателем является класс`type`.

Именно поэтому `type` называют метакласс: метакласс == класс, который создаёт классы.

Получается, что нам надо получить тип `MyClass` и у полученного объекта вызвать метод `__call__`, в который необходимо передать ссылку на сам `MyClass` и дополнительные аргументы, если они нужны.

Проверяем:

-21

Получаем:

-22

Отлично! Наша гипотеза подтвердилась, мы успешно создали экземпляр класса `MyClass` с помощью прямого вызова `__call__` у его метакласса!

И теперь мы снова вернёмся к нашему базовому примеру с кастомным метаклассом, чтобы всё окончательно встало на свои места:

-23

Выполняем и получаем:

-24

И теперь сделаем прямой вызов и проследим всю цепочку:

-25

Получаем:

-26

Вот и вся магия, друзья)) Мы тут наблюдаем эдакий факториал - процесс инстанцирования идентичен, просто разные уровни - в одном случае создаётся экземпляр класса в чистом виде, а в другом - тоже экземпляр класса, только этот экземпляр сам является классом.

Для тех, кто хочет копнуть ещё глубже

Возможно у вас осталось непонимание - как это:

-27

в итоге превращается в это:

-28

В какой момент и кем за нас выполняется превращение `MyClass(10)` в `type(MyClass)`?
Или как `MyClass` передаёт вызов `__call__` своему метаклассу?
На самом деле вызов объекта всегда идёт через `__call__` объекта, который представляет тип этого самого объекта.
В нашем случае типом класса `MyClass` является метакласс `Meta`.

Поэтому Python делает часть работы за нас.

Такой путь зашит в Python: в самом интерпретаторе операция вызова использует слот `tp_call` типа объекта. В нашем случае тип класса `MyClass` — это его метакласс `Meta`.

Это поведение операции вызова мы не можем изменить напрямую. Сейчас узнаем почему.

Может сложиться обманчивое впечатление, что вызов:

-29

эквивалентен:

-30

А первая запись - просто своего рода синтаксический сахар.
Поэтому Python будет сначала искать `__call__` в самом классе `MyClass`.
Если он его там не найдёт, то поднимется к типу объекта (к метаклассу `Meta`) и найдёт `__call__` там.

Но это ошибочное предположение и сейчас мы его легко развенчаем.

Определим у `MyClass` метод класса `__call__` и создадим его экземпляр:

-31

Получаем:

-32

В этом случае у метакласса вызывается `__call__` и экземпляр класса `MyClass` успешно создаётся.

А теперь меняем вызов:

-33

Выполняем:

-34

В этом случае `__call__` вызывается у класса `MyClass` и объект не создаётся.

Поэтому, вызов

-35

не эквивалентен вызову

-36

Это абсолютно два разных вызова у разных объектов!

И ещё немного интересного

Теперь мы уберём у `MyClass` метод `__call__` и попытаемся вызвать напрямую:

-37

Получаем:

-38

Вуаля - и объект создан!
Как? у нас же нет метода `__call__` в `MyClass`?
Дело в том, что при таком прямом вызове Python ищет метод `__call__` у класса `MyClass`.
Он не находит его и идет искать его в цепочке MRO.
Там тоже не находит и идёт к метаклассу `Meta`.
Тут он его находит, происходит вызов и объект успешно создаётся!

Но вдруг такое поведение только у `__call__`?

Давайте ещё добавим кода и убедимся что это работает для любого метода:

-39

Получаем:

-40

Мы стали свиделями двух абсолютно разных действий.

1. Инстанцирование класса с помощью метакласса:

-41

2. Вызов метода напрямую с помощью:

-42

Простой пример использования метакласса

Пример применения метакласса - реализуем паттерн Singleton.
Сделаем так, что бы у класса мог быть только один экземпляр.
Если экземпляр есть - возвращаем ссылку на него. Если нет - создаём и возвращаем инстанс.

-43

Это просто пример - маловероятно, что стоит использовать метакласс для реализации Singleton 😁

Метаклассы часто используются в ORM и различных фреймворках.

Вывод

Друзья, мы немножко прикоснулись к магии Python - пощупали руками метаклассы.
Но заметьте, когда приходит понимание - становится не так страшно.
Бонусом мы разобрались в некоторых смежных нюансах.
В следующей статье посмотрим, как происходит создание самого класса с помощью метакласса.
Надеюсь, вы осилили эту нудную статью до конца 😊

Если я где-то ошибаюсь - пишите в комментарии, будем разбираться💪

-44

Наш сайт:

future-people.ru

Наши курсы:

Python [START]
Git и GitHub [JUNIOR+]
Python для Excel с библиотекой openpyxl
Создание PDF с помощью Python и ReportLab
Python: подготовка к собеседованию Часть 1 [JUNIOR | MIDDLE]

Наши программы:

Python [START] + Git и GitHub [JUNIOR+] 2 курса
Профессия: Python-разработчик [Python | Git | SQL | Linux] 4 курса
Python для работы с Excel и PDF 2 курса

Наши социальные сети:

TelegramYoutube | Dzen