При вызове класса в каком случае Python пропустит вызов init после выполнения new?
Python не вызывает init, если new вернул объект, не являющийся экземпляром вызываемого класса или его подкласса. Если результат new подходит по типу, Python передаёт этот объект в init.
Разделение new и init позволяет отдельно управлять созданием объекта и его инициализацией. Это особенно важно для неизменяемых типов, где состояние нужно задать во время создания, а также для кэширования и наследования встроенных типов.
Такой подход отделяет выделение или выбор экземпляра от настройки его состояния. Поэтому вызов класса не означает безусловный вызов init: он зависит от результата new.
Обычно вызов класса мысленно представляют как последовательность «создать объект, затем инициализировать его». Но new может вернуть уже существующий объект или объект другого типа, поэтому это предположение не всегда верно.
Если разработчик рассчитывает, что init всегда установит инварианты объекта, пропуск инициализации может привести к частично настроенному состоянию. Обратная проблема возникает при кэшировании: init может повторно изменять один и тот же возвращённый объект.
Механизм вызова класса реализуется метаклассом, обычно type. Упрощённо последовательность выглядит так: Python вызывает new, получает результат и проверяет, является ли он экземпляром вызываемого класса или его подкласса.
Если проверка успешна, Python вызывает init для полученного объекта. Если результат имеет несовместимый тип, объект возвращается вызывающему коду без вызова init.
Здесь будет напечатано имя типа object, а сообщение из init не появится: результат new не является экземпляром Factory.
new обычно является статическим методом на уровне механизма вызова, хотя при объявлении в классе его можно определять без явного декоратора staticmethod. Он получает класс и должен вернуть объект, как правило экземпляр этого класса.
Возврат объекта другого типа допустим, но это специальный сценарий: он может использоваться фабриками или прокси, однако делает контракт класса менее очевидным. Возврат существующего экземпляра того же класса не освобождает от init: при каждом обычном вызове класса инициализация может выполниться снова.
Допустим, требуется кэшировать экземпляры неизменяемого значения, например объект, представляющий нормализованный идентификатор. В new можно вернуть уже сохранённый экземпляр, чтобы одинаковые значения разделяли один объект.
Вариант с настройкой состояния в init имеет риск повторной инициализации уже существующего экземпляра. Вариант с возвратом объекта другого типа отключит init, но нарушит ожидаемый тип результата и усложнит использование класса.
Предпочтительное решение — создавать неизменяемое состояние в new, а init сделать пустым или идемпотентным. Это явно учитывает порядок вызовов и предотвращает повреждение состояния при повторном обращении к кэшу.
Да, если возвращённый объект является экземпляром вызываемого класса или его подкласса. Проверяется совместимость типа результата с классом, вызов которого выполнялся; несовместимый объект передаётся дальше без вызова init.
Да. Это используется, например, для кэширования или реализации объектов с единственным экземпляром. Но если возвращённый объект совместим по типу, init обычно будет вызван, поэтому повторную инициализацию нужно предотвращать отдельно или переносить настройку состояния в new.
init не будет вызван, поэтому его проверка параметров и настройка состояния не выполнятся. Вызов класса при этом может завершиться успешно, но результат будет иметь тип, возвращённый new, и не обязан поддерживать интерфейс исходного класса.