Python (e Python C API): __new__ contro __init__


126

La domanda che sto per porre sembra essere un duplicato dell'uso da parte di Python di __new__ e __init__? , ma a prescindere, non mi è ancora chiaro quale sia la differenza pratica tra __new__e __init__.

Prima di affrettarti a dirmi che __new__è per creare oggetti ed __init__è per inizializzare oggetti, fammi essere chiaro: capisco. In effetti, questa distinzione è abbastanza naturale per me, poiché ho esperienza in C ++ in cui abbiamo un posizionamento nuovo , che separa allo stesso modo l'allocazione degli oggetti dall'inizializzazione.

Il tutorial API Python C lo spiega in questo modo:

Il nuovo membro è responsabile della creazione (anziché dell'inizializzazione) di oggetti del tipo. È esposto in Python come __new__()metodo. ... Un motivo per implementare un nuovo metodo è quello di assicurare i valori iniziali delle variabili di istanza .

Quindi, sì - ho capito quello che __new__fa, ma nonostante questo, io ancora non capisco perché è utile in Python. L'esempio fornito dice che __new__potrebbe essere utile se si desidera "assicurare i valori iniziali delle variabili di istanza". Bene, non è esattamente quello __init__che farà?

Nel tutorial dell'API C, viene mostrato un esempio in cui viene creato un nuovo Tipo (chiamato "Noddy") e __new__viene definita la funzione del Tipo . Il tipo Noddy contiene un membro stringa chiamato firste questo membro stringa viene inizializzato su una stringa vuota in questo modo:

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

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

    .....
}

Si noti che senza il __new__metodo definito qui, dovremmo usare PyType_GenericNew, che inizializza semplicemente tutti i membri della variabile di istanza su NULL. Quindi l'unico vantaggio del __new__metodo è che la variabile di istanza inizierà come una stringa vuota, al contrario di NULL. Ma perché questo è mai utile, dal momento che se ci preoccupassimo di assicurarci che le nostre variabili di istanza fossero inizializzate su un valore predefinito, avremmo potuto farlo nel __init__metodo?

Risposte:


137

La differenza sorge principalmente con tipi mutabili vs immutabili.

__new__accetta un tipo come primo argomento e (di solito) restituisce una nuova istanza di quel tipo. Pertanto è adatto per l'uso con tipi mutabili e immutabili.

__init__accetta un'istanza come primo argomento e modifica gli attributi di tale istanza. Ciò è inappropriato per un tipo immutabile, poiché consentirebbe loro di essere modificati dopo la creazione chiamandoobj.__init__(*args) .

Confronta il comportamento di tuplee 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]

Per quanto riguarda il motivo per cui sono separati (a parte semplici motivi storici): i __new__metodi richiedono un gruppo di boilerplate per avere ragione (la creazione iniziale dell'oggetto, e quindi ricordarsi di restituire l'oggetto alla fine). __init__i metodi, al contrario, sono estremamente semplici, dal momento che hai appena impostato tutti gli attributi che devi impostare.

A parte i __init__metodi più facili da scrivere e la distinzione mutabile vs immutabile sopra menzionata, la separazione può anche essere sfruttata per rendere __init__facoltativa la chiamata alla classe genitore in sottoclassi impostando eventuali invarianti di istanze assolutamente richiesti __new__. Questa è generalmente una pratica dubbia - di solito è più chiaro chiamare i __init__metodi della classe genitore come necessario.


1
il codice a cui si fa riferimento come "boilerplate" __new__non è boilerplate, poiché il boilerplate non cambia mai. A volte è necessario sostituire quel particolare codice con qualcosa di diverso.
Miles Rout il

13
La creazione o l'acquisizione in altro modo dell'istanza (di solito con una superchiamata) e la restituzione dell'istanza sono parti necessarie di qualsiasi __new__implementazione e la "piastra di caldaia" a cui mi riferisco. Al contrario, passè un'implementazione valida per __init__- non è richiesto alcun comportamento.
ncoghlan,

37

Probabilmente ci sono altri usi per __new__ ma ce n'è uno davvero ovvio: non puoi sottoclassare un tipo immutabile senza usare __new__. Ad esempio, supponiamo che tu voglia creare una sottoclasse di tupla che può contenere solo valori integrali compresi tra 0 e 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)

Semplicemente non si può fare questo con __init__- se si è tentato di modificare selfin __init__, l'interprete si lamentano del fatto che si sta cercando di modificare un oggetto immutabile.


1
Non capisco perché dovremmo usare super? Voglio dire, perché un nuovo dovrebbe restituire un'istanza della superclasse? Inoltre, come dici tu, perché dovremmo passare cls esplicitamente a nuovi ? super (ModularTuple, cls) non restituisce un metodo associato?
Alcott,

3
@Alcott, penso che tu abbia frainteso il comportamento di __new__. Passiamo clsesplicitamente a __new__perché, come puoi leggere qui, richiede __new__ sempre un tipo come primo argomento. Quindi restituisce un'istanza di quel tipo. Quindi non stiamo restituendo un'istanza della superclasse - stiamo restituendo un'istanza di cls. In questo caso, è esattamente come se avessimo detto tuple.__new__(ModularTuple, tup).
Senderle,

35

__new__()può restituire oggetti di tipi diversi dalla classe a cui è associato. __init__()inizializza solo un'istanza esistente della classe.

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

Questa è la spiegazione più snella finora.
Tarik,

Non del tutto vero. Ho __init__metodi che contengono codice simile self.__class__ = type(...). Questo fa sì che l'oggetto sia di una classe diversa da quella che pensavi di creare. In realtà non posso cambiarlo in un intcome hai fatto ... Ottengo un errore sui tipi di heap o qualcosa del genere ... ma il mio esempio di assegnarlo a una classe creata dinamicamente funziona.
ArtOfWarfare il

Anch'io sono confuso su quando __init__()viene chiamato. Ad esempio, nella risposta di lonetwin , uno Triangle.__init__()o Square.__init__()viene chiamato automaticamente in base al tipo __new__()restituito. Da quello che dici nella tua risposta (e l'ho letto altrove), sembra che nessuno dei due dovrebbe essere dato che Shape.__new__() non restituisce un'istanza di cls(né una di una sua sottoclasse).
martineau,

1
@martineau: i __init__()metodi nella risposta di lonetwin vengono chiamati quando vengono istanziati i singoli oggetti (cioè quando il loro __new__() metodo ritorna), non quando Shape.__new__()ritorna.
Ignacio Vazquez-Abrams,

Ahh, giusto, Shape.__init__()(se ne avesse uno) non verrebbe chiamato. Ora ha tutto più senso ...:¬)
martineau,

13

Non una risposta completa ma forse qualcosa che illustra la differenza.

__new__verrà sempre chiamato quando deve essere creato un oggetto. Ci sono alcune situazioni in cui __init__non verrà chiamato. Un esempio è quando si estraggono oggetti da un file pickle, verranno allocati ( __new__) ma non inizializzati ( __init__).


Chiamerei init da new se volessi allocare memoria e inizializzare i dati? Perché se non esiste nuovo quando si crea init init viene chiamato?
redpix_

2
Il compito del __new__metodo consiste nel creare (ciò implica l'allocazione della memoria) un'istanza della classe e restituirla. L'inizializzazione è un passaggio separato ed è ciò che è normalmente visibile all'utente. Fai una domanda separata se c'è un problema specifico che stai affrontando.
Noufal Ibrahim,

3

Voglio solo aggiungere una parola circa l' intento (in contrasto con il comportamento) di definizione __new__rispetto __init__.

Mi sono imbattuto in questa domanda (tra gli altri) quando stavo cercando di capire il modo migliore per definire una fabbrica di classe. Mi sono reso conto che uno dei modi in cui __new__è concettualmente diverso da quello __init__è il fatto che il vantaggio di __new__è esattamente ciò che è stato affermato nella domanda:

Quindi l'unico vantaggio del metodo __new__ è che la variabile di istanza inizierà come una stringa vuota, al contrario di NULL. Ma perché questo è mai utile, dal momento che se ci preoccupassimo di assicurarci che le nostre variabili di istanza fossero inizializzate su un valore predefinito, avremmo potuto farlo nel metodo __init__?

Considerando lo scenario dichiarato, ci preoccupiamo dei valori iniziali delle variabili di istanza quando l' istanza è in realtà una classe stessa. Quindi, se stiamo creando dinamicamente un oggetto di classe in fase di runtime e dobbiamo definire / controllare qualcosa di speciale sulle istanze successive di questa classe in fase di creazione, definiremmo queste condizioni / proprietà in un__new__ metodo di una metaclasse.

Ero confuso su questo fino a quando non ho davvero pensato all'applicazione del concetto piuttosto che al suo significato. Ecco un esempio che si spera chiarisca la differenza:

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

Nota che questo è solo un esempio dimostrativo. Esistono diversi modi per ottenere una soluzione senza ricorrere a un approccio di fabbrica di classe come sopra e anche se scegliamo di impiantare la soluzione in questo modo, ci sono alcuni avvertimenti lasciati per brevità (ad esempio, dichiarando esplicitamente la metaclasse )

Se stai creando una classe regolare (alias una non metaclasse), allora __new__non ha davvero senso a meno che non sia un caso speciale come lo scenario mutabile contro immutabile nella risposta di risposta di ncoghlan (che è essenzialmente un esempio più specifico del concetto di definizione i valori / proprietà iniziali della classe / del tipo creati tramite __new__per essere quindi inizializzati tramite __init__).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.