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


126

Питання, яке я збираюся задати, здається, є дублікатом використання Python __new__ та __init__? , Але незалежно від того , що до цих пір незрозуміло мені саме те , що практична різниця між __new__і __init__є.

Перш ніж ви поспішите сказати мені, що __new__для створення об'єктів і __init__для ініціалізації об'єктів, дозвольте мені зрозуміти: я це розумію. Насправді, ця відмінність для мене цілком природна, оскільки я маю досвід роботи в C ++, де у нас є нове розміщення , яке аналогічно відокремлює розподіл об'єктів від ініціалізації.

Підручник API Python C пояснює це так:

Новий член відповідає за створення (на відміну від ініціалізації) об'єктів типу. Він розкривається в Python як __new__()метод. ... Однією з причин реалізації нового методу є забезпечення початкових значень змінних екземплярів .

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

У підручнику з API API показаний приклад, коли створюється новий Тип (називається "Noddy") і визначається __new__функція Type . Тип 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__методі?

Відповіді:


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__методи батьківського класу за необхідності.


1
код, на який ви посилаєтесь "котлован" __new__, не є котлован, оскільки котлован ніколи не змінюється. Іноді вам потрібно замінити саме цей код чимось іншим.
Майлз Рут

13
Створення або інший спосіб придбання екземпляра (як правило, з superвикликом) та повернення екземпляра є необхідними частинами будь-якої __new__реалізації, і "котлован", про який я маю на увазі. Навпаки, passце дійсна реалізація для __init__- не потрібно жодної поведінки.
ncoghlan

37

Напевно, є й інші способи використання, __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__інтерпретатор буде скаржитися , що ви намагаєтеся змінити незмінний об'єкт.


1
Я не розумію, навіщо нам використовувати супер? Я маю на увазі, чому новий повинен повернути екземпляр суперкласу? Крім того, як ви сказали, чому ми повинні передати cls явно новим ? super (ModularTuple, cls) не повертає зв'язаний метод?
Алькотт

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

Це поки що найповніше пояснення.
Тарік

Не зовсім правда. У мене є __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__()(якби він був) не називався б. Тепер це має більше сенсу ...:¬)
martineau

13

Не повна відповідь, але, можливо, щось, що ілюструє різницю.

__new__завжди називатиметься, коли об’єкт потрібно створити. Є деякі ситуації, коли __init__не дзвонять. Одним із прикладів є вилучення об'єктів з файлу маринування, вони будуть виділені ( __new__), але не ініціалізовані ( __init__).


Я б закликав init від 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 (це, по суті, більш конкретний приклад концепції визначення початкові значення / властивості класу / типу, що створюється через, __new__а потім ініціалізуватися через __init__).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.