Python (и Python C API): __new__ против __init__

Вопрос, который я собираюсь задать, кажется дубликатом использования Python __new__ и __init__ ?, но, тем не менее, мне все еще непонятно, в чем практическая разница между __new__ и __init__.

Прежде чем вы поспешите сказать мне, что __new__ предназначен для создания объектов, а __init__ - для инициализации объектов, позвольте мне пояснить: Я понимаю. На самом деле, это различие вполне естественно для меня, поскольку у меня есть опыт в C ++, где у нас есть новое размещение, которое аналогичным образом отделяет выделение объекта от инициализации.

В руководстве по Python C API это объясняется следующим образом:

Новый член отвечает за создание (в отличие от инициализации) объектов типа. Он представлен в Python как метод __new__(). ... Одна из причин для реализации нового метода - обеспечить начальные значения переменных экземпляра.

Итак, да - я понимаю то, что делает __new__, но, несмотря на это, я все еще не понимаю, почему это полезно в Python. В приведенном примере говорится, что __new__ может быть полезным, если вы хотите «гарантировать начальные значения переменных экземпляра». Ну, разве это не то, что делает __init__?

В учебнике C API показан пример, в котором создается новый тип (называемый «Noddy») и определяется функция __new__ этого типа. Тип Noddy содержит строковый член с именем first, и этот строковый член инициализируется пустой строкой, например:

static PyObject * Noddy_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    .....

    self->first = PyString_FromString("");
    if (self->first == NULL)
    {
       Py_DECREF(self);
       return NULL;
    }

    .....
}

Обратите внимание, что без определенного здесь метода __new__ нам пришлось бы использовать PyType_GenericNew, который просто инициализирует все члены переменных экземпляра значением NULL. Таким образом, единственным преимуществом метода __new__ является то, что переменная экземпляра будет начинаться с пустой строки, а не с NULL. Но почему это вообще полезно, ведь если бы мы позаботились о том, чтобы переменные нашего экземпляра были инициализированы некоторым значением по умолчанию, мы могли бы просто сделать это в методе __init__?


person Channel72    schedule 01.02.2011    source источник


Ответы (6)


Разница в основном возникает с изменяемыми и неизменяемыми типами.

__new__ принимает тип в качестве первого аргумента и (обычно) возвращает новый экземпляр этого типа. Таким образом, он подходит для использования как с изменяемыми, так и с неизменяемыми типами.

__init__ принимает экземпляр в качестве первого аргумента и изменяет атрибуты этого экземпляра. Это не подходит для неизменяемого типа, поскольку позволяет изменять его после создания путем вызова obj.__init__(*args).

Сравните поведение tuple и list:

>>> x = (1, 2)
>>> x
(1, 2)
>>> x.__init__([3, 4])
>>> x # tuple.__init__ does nothing
(1, 2)
>>> y = [1, 2]
>>> y
[1, 2]
>>> y.__init__([3, 4])
>>> y # list.__init__ reinitialises the object
[3, 4]

Относительно того, почему они разделены (помимо простых исторических причин): __new__ методы требуют набора шаблонов для правильного выполнения (начальное создание объекта и последующее возвращение объекта в конце). __init__ методы, напротив, чрезвычайно просты, поскольку вы просто устанавливаете те атрибуты, которые вам нужно установить.

Помимо упрощения написания __init__ методов и отмеченного выше различия между изменяемым и неизменяемым, разделение также можно использовать для того, чтобы сделать вызов родительского класса __init__ в подклассах необязательным путем установки любых абсолютно необходимых инвариантов экземпляра в __new__. Как правило, это сомнительная практика - обычно проще просто вызвать __init__ методы родительского класса по мере необходимости.

person ncoghlan    schedule 01.02.2011
comment
код, который вы называете шаблоном в __new__, не является шаблоном, потому что шаблон никогда не изменяется. Иногда вам нужно заменить этот конкретный код чем-то другим. - person Miles Rout; 23.01.2013
comment
Создание или получение иным образом экземпляра (обычно с вызовом super) и возврат экземпляра являются необходимыми частями любой реализации __new__ и шаблона, о котором я говорю. Напротив, pass является допустимой реализацией для __init__ - никакого обязательного поведения не требуется. - person ncoghlan; 24.01.2013

Вероятно, есть и другие варианты использования __new__, но есть одно действительно очевидное: нельзя создать подкласс неизменяемого типа без использования __new__. Так, например, предположим, что вы хотите создать подкласс кортежа, который может содержать только целые значения от 0 до size.

class ModularTuple(tuple):
    def __new__(cls, tup, size=100):
        tup = (int(x) % size for x in tup)
        return super(ModularTuple, cls).__new__(cls, tup)

Вы просто не можете сделать это с __init__ - если вы попытаетесь изменить self в __init__, интерпретатор пожалуется, что вы пытаетесь изменить неизменяемый объект.

person senderle    schedule 01.02.2011
comment
Я не понимаю, почему мы должны использовать супер? Я имею в виду, почему new должен возвращать экземпляр суперкласса? Кроме того, как вы выразились, почему мы должны явно передавать cls в new? super (ModularTuple, cls) не возвращает связанный метод? - person Alcott; 19.09.2011
comment
@Alcott, я думаю, вы неправильно понимаете поведение __new__. Мы передаем cls в __new__, потому что, как вы можете прочитать здесь __new__ always требует тип в качестве первого аргумента. Затем он возвращает экземпляр этого типа. Таким образом, мы не возвращаем экземпляр суперкласса - мы возвращаем экземпляр cls. В данном случае это точно так же, как если бы мы сказали tuple.__new__(ModularTuple, tup). - person senderle; 19.09.2011
comment
@senderle. можно ли сказать, что заявление super(ModularTuple, cls).__new__(cls, tup) такое же, как super().__new__(cls, tup) - person sakeesh; 26.08.2020
comment
@sakeesh а, это очень старый ответ, и он не учитывает способ работы Python 3. Что касается Python 3, я думаю, что вы правы, но мне придется поискать детали, чтобы убедиться. - person senderle; 26.08.2020

__new__() может возвращать объекты типов, отличных от класса, к которому он привязан. __init__() только инициализирует существующий экземпляр класса.

>>> class C(object):
...   def __new__(cls):
...     return 5
...
>>> c = C()
>>> print type(c)
<type 'int'>
>>> print c
5
person Ignacio Vazquez-Abrams    schedule 01.02.2011
comment
На данный момент это самое скудное объяснение. - person Tarik; 12.11.2011
comment
Не совсем так. У меня есть __init__ методы, содержащие код, который выглядит как self.__class__ = type(...). Это приводит к тому, что объект относится к классу, отличному от того, который, как вы думали, вы создавали. На самом деле я не могу изменить его на int, как вы ... Я получаю сообщение об ошибке типа кучи или что-то в этом роде ... но мой пример присвоения его динамически создаваемому классу работает. - person ArtOfWarfare; 21.01.2016
comment
Я тоже не понимаю, когда вызывается __init__(). Например, в ответе lonetwin автоматически вызывается Triangle.__init__() или Square.__init__() в зависимости от того, какой тип __new__() возвращается. Судя по тому, что вы говорите в своем ответе (и я читал это в другом месте), похоже, что ни один из них не должен быть, поскольку Shape.__new__() не возвращает экземпляр cls (ни один из подкласс этого). - person martineau; 29.06.2018
comment
@martineau: методы __init__() в ответе lonetwin вызываются, когда создаются экземпляры отдельных объектов (т.е. когда возвращается их __new__() метод), а не когда возвращается Shape.__new__(). - person Ignacio Vazquez-Abrams; 29.06.2018
comment
Ах, да, Shape.__init__() (если бы он был) не вызвали бы. Теперь все обретает смысл ... :¬) - person martineau; 30.06.2018

Не полный ответ, но, возможно, что-то, что иллюстрирует разницу.

__new__ всегда будет вызываться при создании объекта. В некоторых ситуациях __init__ не получают вызова. Один из примеров: когда вы извлекаете объекты из файла pickle, они выделяются (__new__), но не инициализируются (__init__).

person Noufal Ibrahim    schedule 28.02.2011
comment
Могу ли я вызвать init из new, если мне нужно выделить память и инициализировать данные? Почему, если new не существует, при создании экземпляра вызывается init? - person redpix_; 22.12.2015
comment
Задача метода __new__ - создать (это подразумевает выделение памяти) экземпляр класса и вернуть его. Инициализация - это отдельный шаг, который обычно виден пользователю. Если у вас есть конкретная проблема, задайте отдельный вопрос. - person Noufal Ibrahim; 22.12.2015

Просто хочу добавить пару слов о намерении (в отличие от поведения) определения __new__ по сравнению с __init__.

Я столкнулся с этим вопросом (среди прочего), когда пытался понять, как лучше всего определить фабрику классов. Я понял, что одним из аспектов, в котором __new__ концептуально отличается от __init__, является тот факт, что преимущество __new__ - это именно то, что было заявлено в вопросе:

Таким образом, единственным преимуществом метода __new__ является то, что переменная экземпляра будет начинаться с пустой строки, а не с NULL. Но почему это вообще полезно, ведь если бы мы позаботились о том, чтобы наши переменные экземпляра были инициализированы некоторым значением по умолчанию, мы могли бы просто сделать это в методе __init__?

Учитывая заявленный сценарий, мы заботимся о начальных значениях переменных экземпляра, когда экземпляр на самом деле является самим классом. Итак, если мы динамически создаем объект класса во время выполнения и нам нужно определить / контролировать что-то особенное в последующих экземплярах этого создаваемого класса, мы должны определить эти условия / свойства в __new__ методе метакласса.

Я был сбит с толку до тех пор, пока на самом деле не подумал о применении концепции, а не только о ее значении. Вот пример, который, мы надеемся, прояснит разницу:

a = Shape(sides=3, base=2, height=12)
b = Shape(sides=4, length=2)
print(a.area())
print(b.area())

# I want `a` and `b` to be an instances of either of 'Square' or 'Triangle'
# depending on number of sides and also the `.area()` method to do the right
# thing. How do I do that without creating a Shape class with all the
# methods having a bunch of `if`s ? Here is one possibility

class Shape:
    def __new__(cls, sides, *args, **kwargs):
        if sides == 3:
            return Triangle(*args, **kwargs)
        else:
            return Square(*args, **kwargs)

class Triangle:
    def __init__(self, base, height):
        self.base = base
        self.height = height

    def area(self):
        return (self.base * self.height) / 2

class Square:
    def __init__(self, length):
        self.length = length

    def area(self):
        return self.length*self.length

Обратите внимание, это просто наглядный пример. Есть несколько способов получить решение, не прибегая к подходу фабрики классов, как указано выше, и даже если мы решим реализовать решение таким образом, для краткости оставлены небольшие оговорки (например, явное объявление метакласса )

Если вы создаете обычный класс (он же неметакласс), тогда __new__ действительно не имеет смысла, если только это не особый случай, например, сценарий изменяемого и неизменяемого в ответ ncoghlan (который, по сути, является более конкретным примером концепции определения начальных значений / свойств класса / типа, создаваемого с помощью __new__, которые затем инициализируются с помощью __init__).

person lonetwin    schedule 12.04.2017
comment
Это должно привести к тому, что родительский класс __new__() будет вызываться дважды? - person pyansharp; 13.05.2021
comment
nvm, я не осознавал, что классы в этом примере не наследуются от Shape. - person pyansharp; 13.05.2021

Одно из конкретных применений __new__ - сделать класс одноэлементным:

class SingletonClass(object):
  def __new__(cls):
    if not hasattr(cls, 'instance'):
      cls.instance = super(SingletonClass, cls).__new__(cls)
    return cls.instance 

(источник: Шаблон Singleton в Python - Полное руководство - GeeksforGeeks)

person vicmortelmans    schedule 07.04.2021
comment
Одно предостережение к вышеизложенному: инициализация должна выполняться внутри функции __new__. Если у вас есть отдельная функция __init__, она будет выполняться каждый раз при вызове конструктора, что, вероятно, не то, что вам нужно. - person vicmortelmans; 08.04.2021