2011-05-10 10 views
30

Ho riscontrato uno strano bug in python dove l'utilizzo del metodo __new__ di una classe come factory porterebbe al metodo __init__ della classe istanziata da chiamare due volte.Utilizzo di un metodo di classe '__new__ come factory: __init__ viene chiamato due volte

L'idea era originariamente di utilizzare il metodo __new__ della classe madre per restituire un'istanza specifica di uno dei suoi figli in base ai parametri che vengono passati, senza dover dichiarare una funzione di fabbrica al di fuori della classe.

So che l'utilizzo di una funzione di fabbrica sarebbe il miglior modello di progettazione da utilizzare qui, ma cambiare il modello di progettazione a questo punto del progetto sarebbe costoso. La mia domanda quindi è: c'è un modo per evitare la doppia chiamata a __init__ e ottenere solo una singola chiamata a __init__ in uno schema simile?

class Shape(object): 
    def __new__(cls, desc): 
     if cls is Shape: 
      if desc == 'big': return Rectangle(desc) 
      if desc == 'small': return Triangle(desc) 
     else: 
      return super(Shape, cls).__new__(cls, desc) 

    def __init__(self, desc): 
     print "init called" 
     self.desc = desc 

class Triangle(Shape): 
    @property 
    def number_of_edges(self): return 3 

class Rectangle(Shape): 
    @property 
    def number_of_edges(self): return 4 

instance = Shape('small') 
print instance.number_of_edges 

>>> init called 
>>> init called 
>>> 3 

Qualsiasi aiuto molto apprezzato.

risposta

41

Quando si costruisce un oggetto Python chiama il suo metodo __new__ per creare l'oggetto, quindi chiama __init__ sull'oggetto restituito. Quando si crea l'oggetto dall'interno di __new__ chiamando il numero Triangle() che comporterà ulteriori chiamate a __new__ e __init__.

Che cosa si dovrebbe fare è:

class Shape(object): 
    def __new__(cls, desc): 
     if cls is Shape: 
      if desc == 'big': return super(Shape, cls).__new__(Rectangle) 
      if desc == 'small': return super(Shape, cls).__new__(Triangle) 
     else: 
      return super(Shape, cls).__new__(cls, desc) 

che creerà un Rectangle o Triangle senza attivare una chiamata a __init__ e poi __init__ viene chiamato solo una volta.

Modifica per rispondere alla domanda di @ Adrian su opere come super:

super(Shape,cls) ricerche cls.__mro__ trovare Shape e quindi cerca giù il resto della sequenza per trovare l'attributo.

Triangle.__mro__ è (Triangle, Shape, object) e Rectangle.__mro__ è (Rectangle, Shape, object) mentre Shape.__mro__ è solo (Shape, object). Per tutti questi casi, quando si chiama super(Shape, cls) ignora tutto nel mro squence fino a Shape incluso, quindi l'unica cosa rimasta è la tupla a singolo elemento (object,) e viene utilizzata per trovare l'attributo desiderato.

Questo otterrebbe più complicato se si ha un'eredità di diamante:

class A(object): pass 
class B(A): pass 
class C(A): pass 
class D(B,C): pass 

ora un metodo in B potrebbe utilizzare super(B, cls) e se fosse un'istanza B sarebbe cercare (A, object) ma se si ha un esempio D stesso chiamare B cercherebbe (C, A, object) perché il D.__mro__ è (B, C, A, object).

Quindi in questo caso particolare è possibile definire una nuova classe di mixin che modifica il comportamento di costruzione delle forme e si possono ottenere triangoli e rettangoli specializzati che ereditano da quelli esistenti ma costruiti in modo diverso.

+0

Grazie mille, questo ha risolto il mio problema perfettamente. – xApple

+1

Non sarebbe meglio usare 'return Rectangle .__ new __ (Rectangle)' perché questo garantirebbe che '__new__' di' Rectangle' venga chiamato se è definito? –

+2

@Georg, se lo farai dovrai stare molto attento a evitare la ricorsione infinita. Qualsiasi inizializzazione specifica della classe dovrebbe essere in '__init__', quindi penso che sia abbastanza sicuro qui presumere che l'unico lavoro di' __new__' è quello di creare un oggetto del tipo corretto. – Duncan

0

Non riesco a riprodurre questo comportamento in nessuno degli interpreti Python che ho installato, quindi è una sorta di ipotesi. Tuttavia ...

__init__ viene chiamato due volte perché si stanno inizializzando due oggetti: l'oggetto originale Shape e quindi uno della sua sottoclasse. Se modifichi il tuo __init__ in modo da stampare anche la classe dell'oggetto da inizializzare, lo vedrai.

print type(self), "init called" 

Questo è innocuo perché l'originale Shape saranno scartati, dal momento che non si restituisce un riferimento ad esso nel vostro __new__().

Dal richiamo di una funzione è sintatticamente identico ad un'istanza di una classe, è possibile modificare questo per una funzione senza cambiare niente altro, e vi consiglio di fare esattamente questo. Non capisco la tua riluttanza.

+1

Questo non è corretto - il primo 'chiamata __init__' accade all'interno * * l'esterno' chiamata __new__' (quando 'Triangolo()' e 'Rettangolo()' sono chiamati), ma quindi, poiché un'istanza di 'Shape' viene restituita da' __new__', la chiamata 'Shape()' originale richiama '__init__' * di nuovo * su quell'oggetto già inizializzato. Notare che se l'oggetto restituito da '__new __()' è * non * un'istanza di 'Shape()' allora '__init__' non verrà chiamato (che può oscurare i tentativi di osservare questo comportamento se la gerarchia di classi non è destra). – ncoghlan

+0

Infatti, entrambi sono sintatticamente identici. La mia riluttanza deriva dal fatto che se definisco una funzione chiamata "Shape", devo rinominare la mia classe in qualcosa come "_Shape". Ciò causerà alcune ridenominazioni variabili, ovviamente, ma per lo più avrà conseguenze complicate su altre cose come la documentazione generata da sphinx-autodoc. – xApple

+0

Suppongo che tu voglia esporre la documentazione su quelle classi così non puoi semplicemente dichiararle * nella * funzione. Potresti provare a riassegnare l'attributo '__class__' dell'istanza in' __init __() 'e non scherzare con' __new __() '. – kindall

10

Dopo aver postato la mia domanda, ho continuato a cercare una soluzione e ho trovato un modo per risolvere il problema che sembra un po 'un trucco. È inferiore alla soluzione di Duncan, ma ho pensato che potrebbe essere interessante menzionarlo nondimeno. La classe Shape diventa:

class ShapeFactory(type): 
    def __call__(cls, desc): 
     if cls is Shape: 
      if desc == 'big': return Rectangle(desc) 
      if desc == 'small': return Triangle(desc) 
     return type.__call__(cls, desc) 

class Shape(object): 
    __metaclass__ = ShapeFactory 
    def __init__(self, desc): 
     print "init called" 
     self.desc = desc 
+0

Perché dici che questo è inferiore alla soluzione di Duncan. Questo sembra molto più chiaro a quello che sta succedendo, imo meno hacky. Anche meta. –

+1

Ho pensato che fosse meno ovvio perché aggiunge una quarta classe al programma e coinvolge la magia nera "__metaclass__" che non tutti conoscono. – xApple

Problemi correlati