2015-01-18 5 views
5

Ci sono molte domande su SO che chiedono perché python non chiama sempre __init__ dopo la creazione dell'oggetto. La risposta, naturalmente, si trova in questo estratto dalla documentazione:Qual è il motivo di progettazione per il fatto che se __new__ non restituisce un'istanza di cls, python non richiama __init__?

If __new__() restituisce un'istanza di cls, allora il metodo __init__() della nuova istanza verrà invocato come __init__(self[, ...]), dove self è la nuova istanza e la gli argomenti rimanenti sono gli stessi passati a __new__().

Se __new__() non restituisce un'istanza di cls, il metodo __init__() della nuova istanza non verrà richiamato.

Qual è il motivo di progettazione per questo?

+1

Perché "__init__" dovrebbe sempre essere invocato? –

+0

Non ho mai detto che dovrebbe. Mi sto solo chiedendo perché sia ​​così. – DanielSank

+2

La mia ipotesi sarebbe che, se '__new__' non restituisce un'istanza di' cls', Python non ha un valore per 'self' da passare a' __init__'. –

risposta

2

__init__ si comporta come un costruttore. Ha bisogno di un'istanza per fare il suo lavoro come impostare gli attributi e così via. Se __new__ non restituisce esplicitamente restituisce un'istanza, quindi None viene restituito per impostazione predefinita.

Immaginate che cosa succederà quando __init__ ottiene un None come input e sta tentando di impostare gli attributi? Solleverà un'eccezione denominata "AttributeError: 'NoneType' object has no attribute xxxxx".

Quindi penso sia naturale che non invocare __init__ quando __new__ restituisce Nessuno.

0

Se __new__ non restituisce un'istanza di cls, passare il valore di ritorno a cls.__init__ potrebbe portare a cose molto brutte.

In particolare, Python non richiede per restituire un'istanza dello stesso tipo di classe.

Prendete questo esempio inventato:

In [1]: class SillyInt(int): 
    ...:  def __new__(cls): 
    ...:   return "hello" 
    ...:  

In [2]: si = SillyInt() 

In [3]: si 
Out[3]: 'hello' 

per questo abbiamo creato una sottoclasse di int, ma il nostro metodo __new__ restituito un oggetto str.

Non avrebbe molto senso passare quindi la stringa al nostro metodo ereditato int.__init__ per la creazione di istanze. Anche se, nel caso specifico in questione, "funziona" (nel senso di non generare alcun errore), potremmo aver creato un oggetto in uno stato incoerente.


Ok, allora che dire di un esempio più concreto (anche se ancora forzato)?

dire che creiamo due classi che fissano lo stesso attributo come parte della loro inizializzazione (nel metodo __init__):

In [1]: class Tree(): 
    ...:  def __init__(self): 
    ...:   self.desc = "Tree" 
    ...:   

In [2]: class Branch(): 
    ...:  def __init__(self): 
    ...:   self.desc = "Branch" 
    ...:   

Se ora creare una sottoclasse che, in un metodo personalizzato __new__, vuole tornare un diverso tipo di oggetto (che a volte può avere senso nella pratica, anche se non qui!):

In [3]: class SillyTree(Tree): 
    ...:  def __new__(cls): 
    ...:   return Branch() 
    ...:  

Quando un'istanza questa sottoclasse, possiamo vedere che abbiamo un oggetto del tipo previsto, e che l'initialiser Branch è stato quello che è stato chiamato:

In [4]: sillyTree = SillyTree() 

In [5]: sillyTree.desc 
Out[5]: 'Branch' 

cosa sarebbe è successo se Python avesse chiamato incondizionatamente l'inizializzatore ereditato?

Bene, possiamo testare questo chiamando direttamente l'inizializzatore.

cosa si finisce con è un'istanza di Branch che è stato inizializzato come Tree, lasciando l'oggetto in uno stato molto inaspettato:

In [6]: SillyTree.__init__(sillyTree) 

In [7]: sillyTree.desc 
Out[7]: 'Tree' 

In [8]: isinstance(sillyTree, Branch) 
Out[8]: True 

In [9]: isinstance(sillyTree, Tree) 
Out[9]: False 
+0

Questa è una buona risposta, ma è anche confusa perché init non è il costruttore. Nuovo è il costruttore. Chiamare init la funzione di costruzione confonde semplicemente le cose –

+0

Ok ma questo semplicemente spinge via la domanda in modo tale che ora vogliamo chiedere perché vorreste mai una classe 'Foo' tale che' Foo .__ new__' faccia qualcosa di diverso dalle istanze di 'Foo' * e * tale che queste altre istanze non siano inizializzate sensibilmente da 'Foo .__ init__'. Suppongo che si possano immaginare le ragioni per questo, ma avere un esempio reale registrato nella risposta sarebbe davvero d'aiuto. – DanielSank

2

In Python 2, non si può davvero chiamare un metodo regolare con il primo argomento di essere qualcosa di diverso da un'istanza della classe (o una sottoclasse):

class Foo(object): 
    def __init__(self): 
     pass 

Foo.__init__() 
# TypeError: unbound method __init__() must be called with Foo instance as first argument (got nothing instead) 

Foo.__init__(3) 
# TypeError: unbound method __init__() must be called with Foo instance as first argument (got int instance instead) 

Quindi __init__ non si chiama perché non può possibilmente fare qualsiasi altra cosa che sollevare immediatamente un'eccezione. Non cercare di chiamarlo è strettamente più utile (anche se non penso di aver mai visto il codice approfittarne).

Python 3 ha un'implementazione del metodo leggermente più semplice e questa restrizione non è più disponibile, ma la semantica __new__ è la stessa. Non ha senso cercare di eseguire l'inizializzatore di una classe su un oggetto esterno, comunque.


Per una risposta più designy, piuttosto che un "perché è in questo modo" risposta:

Override __new__ è già una cosa strana da fare. Di default, restituisce un oggetto non inizializzato, che è un concetto che Python cerca molto difficile da nascondere. Se si ignora che, probabilmente stai facendo qualcosa di simile:

class Foo(object): 
    def __new__(cls, some_arg): 
     if some_arg == 15: 
      # 15 is a magic number for some reason! 
      return Bar() 
     else: 
      return super(Foo, cls).__new__(cls, some_arg) 

Immaginiamo una variante Python che incondizionatamente chiamato __init__ sul valore di ritorno. Vedo subito una serie di problemi.

Quando ritorni Bar(), dovrebbe Python chiamata Bar.__init__ (che è già stato chiamato in questo caso) o Foo.__init__ (che è per un tipo completamente diverso e si rompono qualunque garanzie Bar fa)?

La risposta deve essere sicuramente quella che viene chiamata Bar.__init__. Significa che devi restituire un'istanza non inizializzata Bar, utilizzando invece il boccone return Bar.__new__(Bar)? Molto raramente Python richiede di chiamare i metodi dunder al di fuori dell'uso di super, quindi sarebbe molto insolito.

Da dove provengono gli argomenti di Bar.__init__? Sia Foo.__new__ e Foo.__init__ vengono passati gli stessi argomenti - quelli passati a type.__call__, che è ciò che gestisce Foo(...). Ma se chiami esplicitamente Bar.__new__, non c'è nessun luogo in cui ricordare gli argomenti che si desidera passare a Bar.__init__.Non è possibile memorizzarli sul nuovo oggetto Bar, perché è ciò che si suppone che lo sia Bar.__init__! E se hai appena detto che ottiene gli stessi argomenti passati a Foo, limiti severamente i tipi che possono essere restituiti da __new__.

Oppure, cosa succede se si desidera restituire un oggetto che esiste già? Python non ha modo di indicare che un oggetto è "già" inizializzato - dal momento che gli oggetti non inizializzati sono una cosa transitoria e per lo più interna solo di interesse per __new__ - quindi non avresti modo di dire di non chiamare di nuovo __init__.

L'approccio attuale è un po 'goffo, ma non penso ci sia un'alternativa migliore. __new__ è supposto per creare spazio di archiviazione per il nuovo oggetto e restituire un diverso tipo di oggetto completamente è solo una cosa davvero strana da fare; questo è il modo meno sorprendente e più utile in cui Python può gestirlo.

Se questa limitazione si sta intralciando, ricorda che l'intera danza __new__ e __init__ è solo il comportamento di type.__call__. Sei perfettamente libero di definire il tuo comportamento __call__ su una metaclass o semplicemente di scambiare la tua classe con una funzione di fabbrica.

+0

Questo è buono ma in qualche modo allontana la domanda invece di rispondere. * Perché * Python 2 non consente di passare un diverso tipo di oggetto in '__init__' di una classe? – DanielSank

+0

fiera! ma poi, quale deve essere chiamato '__init__' - quello sul tipo originale, o quello appartenente al tipo che hai restituito da' __new__'? e perché? se dovessi azzardare un'ipotesi, probabilmente è solo strano e insolito restituire un oggetto _uninitialized_ di un tipo diverso da '__new__', mentre restituire un oggetto non inizializzato del solito tipo è il comportamento predefinito. – Eevee

+0

inoltre, tieni presente che la danza '__new__' /' __init__' è solo l'implementazione di 'type .__ call__', che sei libero di cambiare completamente in una metaclass se lo desideri. – Eevee

1

Questo non è probabilmente la ragione per la progettazione, ma una conseguenza nifty se questa decisione di progettazione è che una classe non deve restituire un'istanza di se stesso:

class FactoryClass(): 
    def __new__(klass, *args, **kwargs): 
     if args or kwargs: 
      return OtherClass(*args, **kwargs) 
     return super().__new__(klass) 

Questo potrebbe essere utile a volte. Ovviamente se stai restituendo un altro oggetto di classe, non vuoi che venga chiamato il metodo init della classe factory.

La vera ragione che spero che l'esempio sopra illustrato illustri, e la linea di fondo, è che qualsiasi altra decisione di progettazione sarebbe priva di senso. New è il costruttore per oggetti di classe Python, non init. Init non è diverso da qualsiasi altro metodo di classe (a parte il fatto che viene auto magicamente chiamato dopo che l'oggetto è stato costruito); indipendentemente dal fatto che qualcuno di loro venga chiamato dipende da cosa succede nel costruttore. Questo è solo un modo naturale di fare le cose. Qualsiasi altro modo sarebbe rotto.

+0

Quella frase "ovviamente" sta elemosinando la domanda :) – DanielSank

+0

Suppongo. Perché vuoi che venga chiamato?Non ha senso. Non esiste alcuna istanza della classe factory. In Python, new è il costruttore, non init. Ti aspetteresti che vengano chiamati altri metodi di classe? Ovviamente no. Non sono nemmeno costruttori. La chiamata a init dipende da cosa succede quando il costruttore fa il suo lavoro. Qualsiasi altra situazione non avrebbe senso. –

+0

E quando la tua decisione progettuale è guidata dall'implementazione alternativa che è del tutto assurda, è probabilmente una di cui puoi essere sicuro. –

Problemi correlati