Python (i API Python C): __new__ kontra __init__


126

Pytanie, które zamierzam zadać, wydaje się być duplikatem użycia przez Pythona funkcji __new__ i __init__? , ale niezależnie od tego, nadal nie jest dla mnie jasne, jaka jest praktyczna różnica między __new__i __init__.

Zanim spieszysz się, aby mi powiedzieć, że __new__służy do tworzenia obiektów i __init__inicjowania obiektów, pozwól mi wyjaśnić: rozumiem. W rzeczywistości to rozróżnienie jest dla mnie całkiem naturalne, ponieważ mam doświadczenie w C ++, gdzie mamy umieszczenie new , które podobnie oddziela alokację obiektów od inicjalizacji.

Samouczek API Python C wyjaśnia to tak:

Nowy członek jest odpowiedzialny za tworzenie (a nie inicjowanie) obiektów tego typu. Jest ujawniany w Pythonie jako __new__()metoda. ... Jednym z powodów wdrożenia nowej metody jest zapewnienie początkowych wartości zmiennych instancji .

Więc tak - rozumiem, co __new__robi, ale mimo to nadal nie rozumiem, dlaczego jest to przydatne w Pythonie. Podany przykład mówi, że __new__może to być przydatne, jeśli chcesz „zapewnić początkowe wartości zmiennych instancji”. Cóż, czy to nie jest dokładnie to, co __init__będzie?

W samouczku interfejsu API języka C pokazano przykład tworzenia nowego typu (zwanego „Noddy”) i __new__definiowania funkcji Type . Typ Noddy zawiera element członkowski typu string o nazwie first, a ten element członkowski ciągu jest inicjowany do pustego ciągu, jak poniżej:

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

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

    .....
}

Zauważ, że bez __new__zdefiniowanej tutaj metody musielibyśmy użyć PyType_GenericNew, która po prostu inicjalizuje wszystkie składowe zmiennej instancji na NULL. Zatem jedyną zaletą tej __new__metody jest to, że zmienna instancji będzie zaczynać się jako pusty ciąg, w przeciwieństwie do NULL. Ale dlaczego jest to kiedykolwiek przydatne, skoro jeśli zależy nam na upewnieniu się, że zmienne instancji są zainicjowane do jakiejś domyślnej wartości, moglibyśmy to zrobić w __init__metodzie?

Odpowiedzi:


137

Różnica pojawia się głównie w przypadku typów zmiennych i niezmiennych.

__new__akceptuje typ jako pierwszy argument i (zwykle) zwraca nowe wystąpienie tego typu. Dlatego nadaje się do stosowania zarówno z typami zmiennymi, jak i niezmiennymi.

__init__akceptuje instancję jako pierwszy argument i modyfikuje atrybuty tej instancji. Jest to nieodpowiednie dla niezmiennego typu, ponieważ pozwoliłoby na ich modyfikację po utworzeniu przez wywołanieobj.__init__(*args) .

Porównaj zachowanie tuplei 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]

Co do tego, dlaczego są oddzielne (poza prostymi powodami historycznymi): __new__metody wymagają do poprawnego wykonania kilku schematów (początkowe utworzenie obiektu, a następnie pamiętanie o zwróceniu obiektu na końcu). __init__W przeciwieństwie do tego metody są śmiertelnie proste, ponieważ po prostu ustawiasz atrybuty, które chcesz ustawić.

Oprócz tego __init__, że metody są łatwiejsze do napisania, a rozróżnienie między modyfikowalnymi a niezmiennymi opisanymi powyżej, separacja może być również wykorzystana, aby wywołanie klasy nadrzędnej __init__w podklasach było opcjonalne, ustawiając wszelkie absolutnie wymagane niezmienniki instancji w __new__. Jest to jednak generalnie wątpliwa praktyka - zwykle jaśniej jest po prostu wywołać __init__metody klasy nadrzędnej , jeśli jest to konieczne.


1
kod, do którego odnosisz się jako „boilerplate” w programie __new__nie jest boilerplate, ponieważ boilerplate nigdy się nie zmienia. Czasami trzeba zamienić ten konkretny kod na coś innego.
Miles Rout

13
Tworzenie lub pozyskiwanie instancji w inny sposób (zwykle za pomocą superwywołania) i zwracanie instancji są niezbędnymi częściami każdej __new__implementacji, a także „schematem wzorcowym”, o którym mówię. Z drugiej strony passjest to poprawna implementacja dla __init__- nie ma żadnego wymaganego zachowania.
ncoghlan

37

Prawdopodobnie istnieją inne zastosowania, __new__ale jest jedno naprawdę oczywiste: nie można podklasować niezmiennego typu bez użycia __new__. Załóżmy na przykład, że chcesz utworzyć podklasę krotki, która może zawierać tylko wartości całkowite z przedziału od 0 do 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)

Po prostu nie możesz tego zrobić __init__- jeśli próbowałeś zmodyfikować selfw __init__, interpreter narzekałby, że próbujesz zmodyfikować niezmienny obiekt.


1
Nie rozumiem, dlaczego powinniśmy używać super? Mam na myśli, dlaczego new miałby zwrócić instancję superklasy? Ponadto, jak to ująłeś, dlaczego mielibyśmy jawnie przekazywać cls do nowego ? super (ModularTuple, cls) nie zwraca powiązanej metody?
Alcott,

3
@Alcott, myślę, że źle rozumiesz zachowanie __new__. Przechodzimy clswyraźnie do __new__ponieważ, jak możesz przeczytać tutaj __new__ zawsze wymaga typu jako pierwszego argumentu. Następnie zwraca instancję tego typu. Więc nie zwracamy instancji superklasy - zwracamy instancję klasy cls. W tym przypadku jest tak samo, jak powiedzieliśmy tuple.__new__(ModularTuple, tup).
senderle

35

__new__()może zwracać obiekty typu innego niż klasa, z którą jest powiązany. __init__()tylko inicjuje istniejące wystąpienie klasy.

>>> class C(object):
...   def __new__(cls):
...     return 5
...
>>> c = C()
>>> print type(c)
<type 'int'>
>>> print c
5

To jest jak dotąd najcieńsze wyjaśnienie.
Tarik

Nie do końca prawda. Mam __init__metody, które zawierają kod, który wygląda jak self.__class__ = type(...). To powoduje, że obiekt należy do innej klasy niż ten, który myślałeś, że tworzysz. Właściwie nie mogę go zmienić na inttak, jak ty ... Otrzymuję błąd dotyczący typów sterty lub czegoś podobnego ... ale mój przykład przypisania go do dynamicznie tworzonej klasy działa.
ArtOfWarfare

Ja też nie wiem, kiedy __init__()jest wezwany. Na przykład, w odpowiedzi lonetwin , zostaniesz wywołany Triangle.__init__()lub Square.__init__()automatycznie, w zależności od tego, który typ __new__()zwróci. Z tego, co powiedziałeś w swojej odpowiedzi (a czytałem to gdzie indziej), wydaje się, że żaden z nich nie powinien być, ponieważ Shape.__new__() nie zwraca wystąpienia cls(ani jednej z jego podklas).
martineau

1
@martineau: __init__()Metody w odpowiedzi lonetwin są wywoływane, gdy tworzona jest instancja poszczególnych obiektów (tj. gdy ich __new__() metoda powraca), a nie gdy Shape.__new__()zwraca.
Ignacio Vazquez-Abrams

Ach, racja, Shape.__init__()(gdyby miał) nie zostałaby wezwana. Teraz wszystko ma więcej sensu ...:¬)
martineau

13

Nie jest to pełna odpowiedź, ale może coś, co ilustruje różnicę.

__new__będzie zawsze wywoływana, gdy trzeba będzie utworzyć obiekt. Są sytuacje, w których __init__nie zostaniesz wezwany. Jednym z przykładów jest to, że gdy usuwasz obiekty z pliku pikle, zostaną one przydzielone ( __new__), ale nie zainicjalizowane ( __init__).


Czy wywołałbym init z nowego, gdybym chciał przydzielić pamięć i zainicjować dane? Dlaczego, jeśli new nie istnieje podczas tworzenia instancji, zostaje wywołany init ?
redpix_

2
Zadaniem __new__metody jest utworzenie (to implikuje alokację pamięci) instancji klasy i zwrócenie jej. Inicjalizacja jest oddzielnym krokiem i jest zwykle widoczna dla użytkownika. Jeśli masz konkretny problem, zadaj osobne pytanie.
Noufal Ibrahim

3

Chcę tylko dodać słowo o zamiarze (w przeciwieństwie do zachowania) definiowania __new__kontra __init__.

Na to pytanie natknąłem się (między innymi), gdy próbowałem zrozumieć najlepszy sposób zdefiniowania fabryki klas. Zdałem sobie sprawę, że jednym ze sposobów, w jaki __new__koncepcyjnie różni się od tego, __init__jest fakt, że korzyść __new__jest dokładnie tym, co zostało stwierdzone w pytaniu:

Zatem jedyną zaletą metody __new__ jest to, że zmienna instancji zaczyna się jako pusty ciąg, w przeciwieństwie do NULL. Ale dlaczego jest to kiedykolwiek przydatne, skoro jeśli zależy nam na upewnieniu się, że zmienne instancji są zainicjowane do jakiejś wartości domyślnej, moglibyśmy to zrobić w metodzie __init__?

Biorąc pod uwagę podany scenariusz, dbamy o początkowe wartości zmiennych instancji, gdy instancja jest w rzeczywistości samą klasą. Tak więc, jeśli dynamicznie tworzymy obiekt klasy w czasie wykonywania i musimy zdefiniować / kontrolować coś specjalnego w kolejnych tworzonych instancjach tej klasy, zdefiniowalibyśmy te warunki / właściwości w__new__ metodzie metaklasy.

Byłem zdezorientowany, dopóki nie pomyślałem o zastosowaniu tego pojęcia, a nie tylko o jego znaczeniu. Oto przykład, który, miejmy nadzieję, wyjaśni różnicę:

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

Zauważ, że to tylko demonstracyjny przykład początkowy. Istnieje wiele sposobów na uzyskanie rozwiązania bez uciekania się do podejścia opartego na fabryce klas, takiego jak powyżej, a nawet jeśli zdecydujemy się wdrożyć rozwiązanie w ten sposób, ze względu na zwięzłość pominięto pewne zastrzeżenia (na przykład jawne zadeklarowanie metaklasy )

Jeśli tworzysz klasę zwykłą (inną niż metaklasa), to __new__nie ma to sensu, chyba że jest to przypadek specjalny, taki jak scenariusz zmienny kontra niezmienny w odpowiedzi ncoghlana (który jest zasadniczo bardziej szczegółowym przykładem pojęcia definiowania początkowe wartości / właściwości klasy / typu tworzonego za pośrednictwem, które __new__mają być następnie zainicjowane za pośrednictwem __init__).

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.