Сообщить об ошибке.

Магический метод object.__new__() в Python

Dunder метод object.__new__()

Специальный метод object.__new__() - это статический метод, который создает новые экземпляры класса и, следовательно, может использоваться для настройки этого процесса.

Dunder методы object.__init__() и object.__new__() могут выглядеть довольно похожими на первый взгляд. Метод object.__init__() инициализирует экземпляр класса, но этот экземпляр должен быть создан до того, как его можно будет инициализировать, и это работа метода dunder object.__new__().

Аргументы магического метода object.__new__

Метод object.__new__ принимает в качестве аргументов класс, экземпляр которого пытаемся создать, и все аргументы, переданные конструктору класса, как показано во фрагменте ниже:

class C:
    def __new__(cls, *args, **kwargs):
        print(cls, args, kwargs)
        return super().__new__(cls)

>>> C()
# <class '__main__.C'> () {}
>>> C(73, True, a=15)  
# <class '__main__.C'> (73, True) {'a': 15}

Оператор return включает в себя super().__new__(cls), что является типичным способом воплощения в жизнь объекта, который необходимо создать.

Возвращаемое значение метода object.__new__

Метод object.__new__ может возвращать любой объект, а возвращаемое значение метода object.__new__ - это результат, который получается при создании экземпляра класса.

Например, если создать класс C, метод __new__ которого возвращает 73, то каждый раз при создании экземпляра класса, C() получим число 73:

class C:
    def __new__(cls):
        return 73

>>> c = C()
>>> print(c)  
# 73
>>> print(type(c))  
# <class 'int'>

Обычно метод object.__new__ возвращает экземпляр класса, в котором он находится. В этом случае Python автоматически вызывает метод object.__init__ для возвращенного объекта и передает аргументы, указанные в конструкторе класса. Таким образом, если object.__init__ вызывается, то он получает те же аргументы, которые были получены object.__new__.

Следующий фрагмент кода показывает, что метод object.__init__ вызывается только в том случае, если возвращаемое значение является экземпляром класса, в котором определен метод object.__new__:

class C:
    def __new__(cls, *, return_73):
        if return_73:
            return 73
        else:
            return super().__new__(cls)

    def __init__(self, *args, **kwargs):
        print("__init__!")

>>> x = C(return_73=True)
>>> print(x)  
# 73
>>> y = C(return_73=False)  
# __init__!
>>> print(y)  
# <__main__.C object at 0x7a88c69c4f20>

Обратите внимание, что Python вызовет object.__init__ для возвращаемого объекта, даже если это экземпляр подкласса класса, в котором выполняется метод object.__new__!

Ниже, во фрагменте кода создаем класс C и его подкласс D. Класс C определяет метод __new__ и оба класса определяют свои соответствующие методы __init__. Когда создаем экземпляр класса C, то фактически получаем экземпляр D, и метод D.__init__ автоматически вызывается:

class C:
    def __new__(cls):
        return super().__new__(D)

    def __init__(self):
        print("C.__init__")

class D(C):
    def __init__(self):
        print("D.__init__")

>>> d = C()  
# D.__init__
>>> print(type(d))  
# <class '__main__.D'>

Это может показаться немного странным, но на самом деле это шаблон, который используется в модуле pathlib.

Дополнительно смотрите:

Как модуль pathlib использует new для настройки создания пути

Когда-нибудь замечали, что если импортировать Path из pathlib, а затем создать его экземпляр, то получим объект, который зависит от операционной системы?

Например, на Linux машине получим следующее:

from pathlib import Path

print(Path())  
# PosixPath('.')

Если бы экземпляр создавался на машине Windows, то получили WindowsPath вместо PosixPath. Как модуль это делает, если только создается экземпляр одного и того же класса Path?

Класс Path реализует метод __new__, а метод __new__ проверяет операционную систему, на которой он запущен. Если это машина Windows, то он создаст WindowsPath. Если это НЕ Windows, он создаст PosixPath. Поскольку WindowsPath и PosixPath унаследованы от Path, это не повлияет на оставшуюся часть создания и инициализации объекта.

Фрагмент кода ниже имитирует эту структуру (с бесполезным, но гораздо более простым примером):

class Number:
    def __new__(cls, value):
        print("Number.__new__", end=", ")
        if cls is Number:
            cls = OddNumber if value % 2 else EvenNumber
        return super().__new__(cls)

    def __init__(self, value):
        print("Number.__init__")
        self.value = value

class EvenNumber(Number):
    def __init__(self, value):
        print("EvenNumber.__init__", end=", ")
        super().__init__(value)
        self.is_even = True

class OddNumber(Number):
    def __init__(self, value):
        print("OddNumber.__init__", end=", ")
        super().__init__(value)
        self.is_even = False

>>> x = Number(73)  
# Number.__new__, OddNumber.__init__, Number.__init__
>>> print(type(x))  
# <class '__main__.OddNumber'>
>>> print(x.value)  
# 73