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

127

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

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

В руководстве по 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__методе?

Channel72
источник

Ответы:

137

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

__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__методы родительского класса по мере необходимости.

ncoghlan
источник
1
код, который вы называете "шаблоном", __new__не является шаблонным, потому что шаблон никогда не изменяется. Иногда вам нужно заменить этот конкретный код чем-то другим.
Miles Rout
13
Создание или иное получение экземпляра (обычно с superвызовом) и возврат экземпляра являются необходимыми частями любой __new__реализации и «шаблоном», о котором я говорю. Напротив, passэто допустимая реализация для __init__- вообще не требуется никакого поведения.
ncoghlan
38

Вероятно, есть и другие варианты использования, __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__- если вы попытаетесь изменить selfin __init__, интерпретатор пожалуется, что вы пытаетесь изменить неизменяемый объект.

senderle
источник
1
Я не понимаю, зачем нам использовать супер? Я имею в виду, почему new должен возвращать экземпляр суперкласса? Кроме того, как вы выразились, почему мы должны явно передавать cls в new ? super (ModularTuple, cls) не возвращает связанный метод?
Alcott
3
@Alcott, я думаю, вы неправильно понимаете поведение __new__. Мы передаемся clsявно, __new__потому что, как вы можете прочитать здесь, __new__ всегда требуется тип в качестве первого аргумента. Затем он возвращает экземпляр этого типа. Таким образом, мы не возвращаем экземпляр суперкласса - мы возвращаем экземпляр cls. В данном случае все так же, как если бы мы сказали tuple.__new__(ModularTuple, tup).
senderle
35

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

>>> class C(object):
...   def __new__(cls):
...     return 5
...
>>> c = C()
>>> print type(c)
<type 'int'>
>>> print c
5
Игнасио Васкес-Абрамс
источник
На данный момент это самое скудное объяснение.
Tarik
Не совсем так. У меня есть __init__методы, содержащие код, похожий на self.__class__ = type(...). Это приводит к тому, что объект относится к классу, отличному от того, который, как вы думали, создавали. На самом деле я не могу изменить его на, intкак вы ... Я получаю сообщение об ошибке типа кучи или что-то в этом роде ... но мой пример назначения его динамически создаваемому классу работает.
ArtOfWarfare
Я тоже не понимаю, когда __init__()называется. Например, в ответе lonetwin в , либо Triangle.__init__()или Square.__init__()автоматически вызываются в зависимости от того, какого типа __new__()возвращается. Из того, что вы говорите в своем ответе (и я читал это в другом месте), не похоже, что ни один из них должен быть, поскольку Shape.__new__() не возвращает экземпляр cls(или один из его подклассов).
Мартино
1
@martineau: __init__()методы в ответе lonetwin вызываются, когда создаются экземпляры отдельных объектов (т.е. когда их __new__() метод возвращается), а не при Shape.__new__()возврате.
Игнасио Васкес-Абрамс
Ах, да, Shape.__init__()(если бы он был) не вызвали бы. Теперь все :¬)
обретает
13

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

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

Нуфаль Ибрагим
источник
Могу ли я вызвать init из new, если мне нужно выделить память и инициализировать данные? Почему, если new не существует, при создании экземпляра вызывается init ?
redpix_
2
Задача __new__метода - создать (это подразумевает выделение памяти) экземпляр класса и вернуть его. Инициализация - это отдельный шаг, и он обычно виден пользователю. Если у вас есть конкретная проблема, задайте отдельный вопрос.
Нуфал Ибрагим
3

Просто хочу , чтобы добавить слово о намерении (в отличии от поведения) определений в __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 answer (который, по сути, является более конкретным примером концепции определения начальные значения / свойства создаваемого класса / типа, которые __new__затем инициализируются с помощью __init__).

lonetwin
источник