Метакласс __call__ vs __new__/__init__: порядок вызовов в Python

Метакласс __call__ vs __new__/__init__: порядок вызовов в Python

@python_quiz

Разберем этот квиз

Коротко: при создании экземпляра класса сначала вызывается __call__ метакласса, а уже внутри него (если он делегирует) — __new__ и __init__ самого класса. Разберём это на примере, представленном в квизе.

Исходный код

Вот код из задания — он компактный и использует трюк с кортежем, чтобы печатать строку и вернуть результат второго выражения:

python

Если запустить этот код, на экран попадут три строки:
M.__call__
A.__new__
A.__init__

Теперь разберём, почему именно в таком порядке.

Разбор по шагам

  1. Вызов A() интерпретируется как вызов __call__ у метакласса класса A. Для классов стандартным метаклассом является type, а в нашем примере метакласс — M, у которого переопределён __call__.
  2. Внутри M.__call__ выполняется print("M.__call__"), затем выполняется делегирование к стандартному type.__call__(cls, *a, **k).
  3. Поведение type.__call__ — оно:

- вызывает cls.__new__(cls, *a, **k) для создания экземпляра,
- затем вызывает cls.__init__(экземпляр, *a, **k) для инициализации,
- и возвращает экземпляр.

  1. У класса A определён __new__, который печатает "A.__new__" и возвращает объект через object.__new__(cls).
  2. У класса A также определён __init__, который печатает "A.__init__".

Следовательно, первым печатается строка из метакласса, затем — из __new__, затем — из __init__.

Почему автор кода использовал кортеж с [1]

В лямбдах в коде используется конструкция (print(...), expr)[1]. Это трюк:

  • Сначала выполняется print(...), он возвращает None,
  • затем вычисляется expr и возвращается как второй элемент кортежа.

Таким образом достигается эффект "напечатать и вернуть значение выражения" в одной строчке лямбды.

Более читаемый эквивалент — определение методов явно:

python

Этот код даёт тот же вывод и легче читается.

Анализ вариантов ответов (почему они верны или нет)

  • Вариант, в котором печатаются только A.__new__ и A.__init__: неверен, потому что вызов A() сначала идёт через __call__ метакласса, и у нас есть явный print в M.__call__, поэтому эта строка будет выведена первой.
  • Вариант, в котором печатается только M.__call__: неверен, потому что M.__call__ делегирует работу type.__call__, а тот в свою очередь вызывает __new__ и __init__ класса A, которые тоже печатают строки.
  • Вариант с тремя печатями (M.__call__, A.__new__, A.__init__): соответствует реальному поведению кода по описанным выше причинам.
  • Вариант про ошибку из-за "перекрытия __new__": неверно. Переопределение __call__ метакласса само по себе не вызывает ошибки. Ошибка могла бы произойти, если бы метакласс __call__ не возвращал корректный объект или не вызывал делегирование, но в представленном коде делегирование выполняется и всё корректно.

Дополнительно: что если метакласс не делегирует?

Если в M.__call__ не вызывать type.__call__ (или super().__call__), то поведение изменится — экземпляр может не создаться, __new__/__init__ не будут вызваны, либо метакласс может сам вернуть что-то другое. Пример:

python

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

Вывод

При вызове класса с нестандартным метаклассом сначала вызывается __call__ метакласса. Если метакласс делегирует работу стандартному type.__call__, то внутри этого делегированного вызова будут последовательно вызваны __new__ и __init__ самого класса. В представленной задаче вывод состоит из трёх строк в порядке: M.__call__, A.__new__, A.__init__.

Report Page