2015-12-11 15 views
16

Sono curioso di sapere se esiste un modo in Python per forzare (dalla classe Parent) affinché un metodo genitore venga chiamato da una classe figlia quando viene sovrascritto.Forza la classe figlio a chiamare il metodo padre quando lo sovrascrive.

Esempio:

class Parent(object): 

    def __init__(self): 
     self.someValue=1 

    def start(self): 
     ''' Force method to be called''' 
     self.someValue+=1 

La corretta attuazione di quello che volevo il mio bambino di classe Bambino fare sarebbe:

class ChildCorrect(Parent): 

    def start(self): 
     Parent.start(self) 
     self.someValue+=1 

Tuttavia, c'è un modo per forzare gli sviluppatori che sviluppano classi figlie di in particolare chiama (non solo sostituisci) il metodo "start" definito nella classe Parent nel caso in cui dimentichi di chiamarlo:

class ChildIncorrect(Parent): 

    def start(self): 
     '''Parent method not called, but correctly overridden''' 
     self.someValue+=1 

Inoltre, se questa non è considerata la migliore pratica, quale sarebbe un'alternativa?

+0

Il metodo di sovrascrittura deve avere libertà * quando * chiama super? Cioè andrebbe bene se il metodo di override fosse costretto a chiamare super all'inizio o alla fine della sua esecuzione? (Probabilmente, non è corretto limitarlo nel caso generale.) Se lo è, un metaclasse può essere usato per avvolgere qualsiasi metodo di override e chiamare implicitamente super; il metodo di override non sarebbe quindi autorizzato a chiamare super da solo (che potresti non essere in grado di applicare). –

+0

Che dire di un utente dovrebbe chiamare il metodo della superclasse, simile al modo in cui le implementazioni del metodo astratto mancanti vengono rilevate con un linter? – pnovotnak

risposta

14

Come (non) si fa

No, non c'è modo sicuro per obbligare gli utenti a chiamare eccellente. Esaminiamo alcune opzioni che potrebbero raggiungere questo obiettivo o un obiettivo simile e discutere perché è una cattiva idea.Nella prossima sezione, discuterò anche di quale sia il modo sensato (rispetto alla comunità Python) per affrontare la situazione.

  1. Una metaclasse potrebbe controllare, al momento è definita la sottoclasse, se un metodo override il metodo di destinazione (= ha lo stesso nome del metodo di destinazione) chiama eccellente con gli argomenti appropriati.

    Ciò richiede un comportamento specifico dell'implementazione, come l'utilizzo del modulo dis di CPython. Non ci sono circostanze in cui potrei immaginare che questa sia una buona idea, dipende dalla specifica versione di CPython e dal fatto che stai usando CPython.

  2. Un metaclasse può cooperare con la baseclass. In questo scenario, la classe di base notifica il metaclasse quando viene chiamato il metodo ereditato e il metaclasse esegue il wrapping di qualsiasi metodo di override con un pezzo di codice di guardia.

    Il codice di guardia verifica se il metodo ereditato è stato chiamato durante l'esecuzione del metodo di sovrascrittura. Questo ha il rovescio della medaglia che un canale di notifica separato per ogni metodo che necessita di questa "caratteristica" è richiesto, e inoltre la sicurezza del thread e il rientro sarebbero una preoccupazione. Inoltre, il metodo di sovrascrittura ha terminato l'esecuzione nel momento in cui si nota che non ha chiamato il metodo ereditato, il che potrebbe essere negativo a seconda del proprio scenario.

    Si apre anche la domanda: Cosa fare se il metodo di sovrascrittura non ha chiamato il metodo ereditato? Alzare un'eccezione potrebbe essere inaspettata dal codice che utilizza il metodo (o, peggio ancora, potrebbe presumere che il metodo non ha avuto alcun effetto, il che non è vero).

    Anche il feedback tardivo dello sviluppatore che sovrascrive la classe (se ottiene quel feedback per niente!) È negativo.

  3. Una metaclasse potrebbe generare un pezzo di codice di guardia per ogni metodo overriding che chiama il metodo ereditato automaticamente prima o dopo il metodo predominante ha eseguito.

    Il lato negativo qui è che gli sviluppatori non si aspetteranno che il metodo ereditato venga chiamato automaticamente e non abbia modo di interrompere quella chiamata (ad esempio se una precondizione speciale per la sottoclasse non viene soddisfatta) o controllare quando nel proprio metodo di sovrascrittura il metodo ereditato è chiamato.

    Questo viola un buon insieme di principi sensibili durante la codifica di Python (evitare sorprese, l'esplicito è meglio di implicito, e possibilmente di più).

  4. Una combinazione del punto 2 e 3. Utilizzo della cooperazione di base-e meta-classe dal punto 2, il codice di protezione dal punto 3 potrebbe essere esteso a richiamare automaticamente eccellente sse il metodo prevalente non ha chiamato eccellente loro stessi.

    Ancora una volta, questo è inaspettato, ma risolve i problemi con super chiamate duplicate e come gestire un metodo che non chiama super.

    Tuttavia, ci sono ancora problemi rimanenti. Mentre la sicurezza del thread potrebbe essere risolta con i thread-locals, non c'è ancora modo per un metodo di override di annullare la chiamata a super quando non è soddisfatta una condizione preliminare, eccetto sollevando un'eccezione che potrebbe non essere desiderabile in tutti i casi. Inoltre, super può essere chiamato automaticamente dopo il, il metodo di sovrascrittura, non prima, che, di nuovo, in alcuni scenari è indesiderabile.

Inoltre, nessuno di questo aiuto contro re-binding dell'attributo durante la vita dell'oggetto e classe, anche se questo può essere aiutato mediante descrittori e/o aumentare il metaclasse di prendersi cura di esso.

Perché non farlo

Inoltre, se questo non è considerata best practice, quale sarebbe un'alternativa?

Una pratica comune con Python è quella di presumere che si sia degli adulti consenzienti. Ciò significa che nessuno sta attivamente cercando di fare cose cattive al tuo codice, a meno che tu non lo permetta. In un tale ecosistema, avrebbe senso attaccare uno .. warning:: nella documentazione del metodo o della classe, in modo che chiunque erediti da quella classe sappia cosa devono fare.

Inoltre, chiamando il metodo eccellente in un punto adeguato senso in tanti contesti che gli sviluppatori che utilizzano la classe di base lo considererà in ogni modo e dimenticare solo accidentalmente. In tal caso, l'utilizzo della metaclasse dal terzo punto sopra non aiuterebbe gli utenti che dovrebbero ricordare non per chiamare super, il che potrebbe essere un problema di per sé, in particolare per i programmatori esperti.

Inoltre viola il principio di minima sorpresa e "esplicito è meglio di implicito" (nessuno si aspetta che il metodo ereditato venga chiamato implicitamente!). Questo, ancora una volta, dovrebbe essere ben documentato, nel qual caso si può anche ricorrere a solo che non hanno super-essere chiamato automaticamente e proprio documento che rende ancora più senso del solito per chiamare il metodo ereditato.

-1

Tutto ciò che serve è super.

Con esso, il codice potrebbe essere la seguente:

class Parent(object): 
    def __init__(self): 
     self.someValue = 1 

    def start(self): 
     self.someValue += 1 


class Child(Parent): 
    def start(self): 
     # put code here 
     super().start() 
     # or here 
+2

Grazie per la risposta! Tuttavia, ciò a cui mi riferisco è se c'è qualcosa che posso fare per il metodo definito nella classe Parent per forzare la sua chiamata anche se è sovrascritta. Cosa accadrebbe se uno sviluppatore non lo chiamasse: super (Child, self) .start() – Spektor

+0

Ugh, ti ho frainteso. Quindi, penso che sia impossibile in Python la costruzione dell'ereditarietà - puoi sovrascrivere ogni funzione, quindi non c'è alcuna garanzia che le tue funzioni vengano chiamate. Anche se, Python ha convenzione che metodi/proprietà che nomi inizia con singolo trattino basso (ad es. _my_method) sono "protetti" e di utilizzarli a proprio rischio. – andrzej3393

1

Tuttavia, c'è un modo per forzare gli sviluppatori che sviluppano classi figlio chiamare specifico (non solo override) il metodo di 'start' definito nella classe genitore nel caso in cui si dimentica di chiamarlo

in Python non è possibile controllare come gli altri hanno la precedenza le funzioni.

+1

Verifica se è possibile utilizzare 'metaclass' per questa richiesta. –

+1

Un metacarpo non sarà di alcun aiuto. In questo contesto, la cosa più utile che può fare è rilevare che la funzione è stata sovrascritta. Questo non è sufficiente però.Dovresti controllare che chiami in modo super appropriato (il che non può essere fatto senza fare affidamento su un comportamento definito dall'implementazione come il modulo '' dis'' in CPython o in realtà chiamando il metodo e tracciando la chiamata) e che non ottiene assegnato durante la vita dell'istanza. Tracciare la chiamata quando viene chiamato il metodo può essere eseguita con un metaclasse, ma sicuramente è eccessivo. –

2

Molto poco in Python significa forzare altri programmatori a codificare in un determinato modo. Anche nella libreria standard troverete avvertimenti such as this:

Se la sottoclasse sovrascrive il costruttore, deve fare in modo di richiamare il costruttore della classe base prima di fare qualsiasi altra cosa al thread.

Mentre sarebbe possibile utilizzare un metaclasse per modificare il comportamento sottoclassi, il migliore possibile in modo affidabile fare in questo caso sarebbe di sostituire i nuovi metodi con ancora un altro metodo che potrebbe prendersi cura di convocazione sia la sostituzione metodo e il metodo originale. Tuttavia, ciò avrebbe problemi se il programmatore, al momento , chiamasse il metodo genitore nel nuovo codice,, in più avrebbe portato a cattive abitudini di non chiamare i metodi padre quando si dovrebbe.

In altre parole, Python non è costruito in questo modo.

+0

Grazie per la spiegazione; questo è quello che volevo capire e certamente non voglio implementare qualcosa che non sia pitonico. Ho pensato che forse, forse ci sarebbe un tipo nascosto di metodo che potrei usare per far rispettare questo. – Spektor

2

Con alcuni hack metaclass, è possibile rilevare in fase di esecuzione se l'avvio genitore è stato chiamato o meno. Comunque, sicuramente userò il recommand usando questo codice in situazioni reali, probabilmente è buggato in molti modi e non è pitonioso far rispettare tali restrizioni.

class ParentMetaClass(type): 

    parent_called = False 

    def __new__(cls, name, bases, attrs): 
     print cls, name, bases, attrs 
     original_start = attrs.get('start') 
     if original_start: 
      if name == 'Parent': 
       def patched_start(*args, **kwargs): 
        original_start(*args, **kwargs) 
        cls.parent_called = True 

      else: 
       def patched_start(*args, **kwargs): 
        original_start(*args, **kwargs) 
        if not cls.parent_called: 
         raise ValueError('Parent start not called') 
        cls.parent_called = False 

      attrs['start'] = patched_start 

     return super(ParentMetaClass, cls).__new__(cls, name, bases, attrs) 


class Parent(object): 
    __metaclass__ = ParentMetaClass 

    def start(self): 
     print 'Parent start called.' 


class GoodChild(Parent): 
    def start(self): 
     super(GoodChild, self).start() 
     print 'I am a good child.' 


class BadChild(Parent): 
    def start(self): 
     print 'I am a bad child, I will raise a ValueError.' 
5

Se la gerarchia delle classi è sotto il vostro controllo, è possibile utilizzare ciò che la Banda dei Quattro (Gamma, et al) Design Patterns libro chiama il pattern Template Method:

class MyBase: 
    def MyMethod(self): 
     # place any code that must always be called here... 
     print "Base class pre-code" 

     # call an internal method that contains the subclass-specific code 
     self._DoMyMethod() 

     # ...and then any additional code that should always be performed 
     # here. 
     print "base class post-code!" 

    def _DoMyMethod(self): 
     print "BASE!" 


class MyDerived(MyBase): 
    def _DoMyMethod(self): 
     print "DERIVED!" 


b = MyBase() 
d = MyDerived() 

b.MyMethod() 
d.MyMethod() 

uscite:

Base class pre-code 
BASE! 
base class post-code! 
Base class pre-code 
DERIVED! 
base class post-code! 
+0

E quindi documentare che il metodo privato è quello da sovrascrivere. +1 per la tecnica, ma sconsiglio effettivamente di usarlo. ;) –

+0

@EthanFurman Hm, perché? Avere sia il pre-codice che il post-codice che devono essere eseguiti mi sembra un caso d'uso ragionevole (non necessariamente per la domanda iniziale). Ci sono idiomi più belli da usare qui? –

+0

@JonasWielicki: È un caso d'uso ragionevole, ma non è quello che l'OP aveva bisogno. Per adattarsi al caso dell'OP dovrebbe ancora essere documentato, nel qual caso si può anche solo documentare che il metodo genitore deve essere chiamato. –

Problemi correlati