Представьте, что вы пишете собственный синглтон или перехватываете создание неизменяемых объектов, уверены в своем коде — и вдруг интерпретатор падает с глупым TypeError. Знакомо? В этот момент разработчик обычно смотрит на метод __new__ так, будто тот внезапно заговорил на клингонском (или выдал ошибку линтера, которую вы трое суток безуспешно пытались отключить). Давайте разберем этот механизм изнутри, ведь когда мы пишем систему кэширования конфигураций или кастомный фреймворк, понимание того, как Python крутит шестеренки инстанцирования, спасает часы дебага.
Введение в модель создания объектов в Python
Каждый разработчик, который серьезно изучал объектно-ориентированное программирование на Python, рано или поздно сталкивается с магическими методами. Среди них особое место занимают __init__ и __new__. Большинство программистов привыкли использовать __init__ для инициализации уже созданного объекта (и надеяться, что там не всплывет легаси из 2017 года), но когда дело доходит до контроля самого процесса инстанцирования, на сцену выходит __new__. Это статический метод по своей природе, который отвечает за создание и возвращение нового экземпляра класса.
Однако при работе с этим методом разработчики часто натыкаются на неочевидные подводные камни. Один из самых интригующих вопросов звучит так: почему и как Python ведет себя совершенно по-разному, когда метод __new__ вызывается напрямую из экземпляра класса, а не из самого класса? Понимание этого механизма требует погружения во внутреннее устройство интерпретатора CPython, дескрипторы и метаклассы.
Но прежде чем заглянуть под капот интерпретатора и посмотреть, как именно CPython связывает методы воедино, освежим в памяти базовую механику.
Анатомия метода __new__ и его отличие от __init__
Прежде чем анализировать поведение метода при вызове из экземпляра, вспомним базовую архитектуру. Когда мы пишем выражение вида obj = MyClass(), интерпретатор Python выполняет два четких шага:
- Вызывает метод
MyClass.__new__(cls, *args, **kwargs), который должен вернуть новый экземпляр класса (обычно путем делегирования родительскому методу черезsuper().__new__(cls)). - Если возвращенный объект является экземпляром класса
MyClass, интерпретатор автоматически вызывает методobj.__init__(*args, **kwargs), передавая в него свежесозданный объект в качествеself.
Обратите внимание на ключевой нюанс: __new__ принимает в качестве первого аргумента сам класс (обычно обозначаемый как cls), в то время как __init__ принимает готовый экземпляр (self). Из-за этого сигнатура __new__ больше похожа на сигнатуру методов класса (@classmethod), но с точки зрения дескрипторного протокола Python обрабатывает его особым образом.
class Sample:
def __new__(cls, value):
print(f"Вызван __new__ для класса {cls}, значение: {value}")
instance = super().__new__(cls)
return instance
def __init__(self, value):
print(f"Вызван __init__ для объекта {self}")
self.value = value
# Стандартный сценарий
obj = Sample(42)
Именно эта двойственность — между сигнатурой метода класса и поведением статического метода — создает почву для удивительных сюрпризов, когда мы обращаемся к __new__ через уже готовый объект (это как пытаться починить сервер по SSH, находясь при этом внутри запущенного на нем же контейнера, который вы случайно удалили).
Что происходит при вызове __new__ из экземпляра?
В CPython методы обрабатываются через протокол дескрипторов (descriptor protocol). Когда вы обращаетесь к методу через экземпляр класса (например, obj.__new__), срабатывает метод __get__ этого атрибута. Для обычных методов экземпляр автоматически подставляется в качестве первого аргумента (self).
Но с __new__ ситуация выглядит иначе. Поскольку __new__ концептуально ближе к статическим методам