Что произойдёт при создании экземпляра, если метод init явно вернёт значение, отличное от None?
Создание экземпляра завершится исключением TypeError. Метод init обязан вернуть именно None; обычно он ничего не возвращает явно.
Важно: возвращаемое значение init не становится результатом вызова класса. Экземпляр уже создан методом new, а init только инициализирует его.
Разделение new и init позволяет отделить создание объекта от его настройки. Это особенно важно для неизменяемых типов, где состояние часто нужно задать уже во время new, до завершения создания экземпляра.
Такой протокол делает поведение вызова класса единообразным: сначала получается объект, затем он инициализируется. Поэтому init задуман как процедура настройки, а не как фабрика, возвращающая произвольный объект.
Ошибка часто появляется, когда разработчик по привычке пишет в init return self или возвращает результат вспомогательной функции. В отличие от обычной функции, init не должен сообщать вызывающему коду созданный объект.
Неверный возврат обнаруживается во время вызова класса, поэтому объект может быть уже выделен и частично инициализирован к моменту возникновения TypeError. Нельзя рассчитывать на такой объект как на успешно созданный экземпляр.
Протокол вызова класса концептуально выполняет следующие шаги:
Минимальный пример:
Здесь возвращается экземпляр User, но это всё равно недопустимо. Типичный текст ошибки указывает, что init должен вернуть None, а не объект User.
Явный return None допустим, но обычно избыточен. Если конструктору нужна фабричная логика, следует использовать classmethod, отдельную функцию-фабрику или, для специальных случаев создания объектов, корректно реализовать new.
Нужно отличать это правило от поведения new. Именно new может вернуть экземпляр другого класса; тогда обычный вызов init исходного класса не выполняется. Это отдельный механизм и не превращает init в метод, который может возвращать произвольный результат.
В библиотеке создавался объект конфигурации. Разработчик хотел сразу вернуть кэшированный экземпляр и написал возврат объекта из init. При каждом вызове Python успевал создать новый объект, затем отклонял результат init с TypeError, поэтому кэширование не работало.
Рассматривались варианты:
Выбрали classmethod-фабрику, потому что кэширование было частью бизнес-логики, а не фундаментальным свойством размещения объекта в памяти. init оставили только для установки состояния и без возвращаемого значения. В результате контракт класса стал очевидным, а повторное создание и побочные эффекты контролировались в одном месте.
Да. new должен вернуть объект, обычно экземпляр создаваемого класса или его подкласса. Именно его Python передаст в init, если тип результата подходит. Возвращение объекта другого типа может привести к пропуску init исходного класса.
Вызов класса завершится этим исключением, а экземпляр не будет выдан вызывающему коду как успешно созданный. При этом сам факт выделения объекта до ошибки не означает, что его состояние можно безопасно использовать.
Потому что его контракт фиксирован: он инициализирует уже выбранный объект и возвращает None. Если требуется выбрать между разными типами, вернуть кэшированный экземпляр или использовать разные способы создания, это следует выражать через new либо явную фабрику; такой дизайн лучше отражает намерение и не нарушает протокол объектов Python.