Sostituisci un metodo a livello di istanza


87

C'è un modo in Python per sovrascrivere un metodo di classe a livello di istanza? Per esempio:

class Dog:
    def bark(self):
        print "WOOF"

boby = Dog()
boby.bark() # WOOF
# METHOD OVERRIDE
boby.bark() # WoOoOoF!!

Risposte:


10

Si prega di non farlo come mostrato. Il codice diventa illeggibile quando si esegue il monkeypatch di un'istanza in modo che sia diversa dalla classe.

Non è possibile eseguire il debug del codice con patch di scimmia.

Quando trovi un insetto in bobye print type(boby), vedrai che (a) è un cane, ma (b) per qualche oscuro motivo non abbaia correttamente. Questo è un incubo. Non farlo.

Per favore, fallo invece.

class Dog:
    def bark(self):
        print "WOOF"

class BobyDog( Dog ):
    def bark( self ):
        print "WoOoOoF!!"

otherDog= Dog()
otherDog.bark() # WOOF

boby = BobyDog()
boby.bark() # WoOoOoF!!

9
@arivero: pensavo che "Per favore, non farlo come mostrato" lo rendesse perfettamente chiaro. Quali altre o diverse parole vorresti vedere per rendere più chiaro che questo non sta rispondendo alla domanda che è stata posta, ma sta fornendo consigli sul perché è una cattiva idea?
S.Lott

46
Non sono in disaccordo nel consiglio, né nell'OP come sembra. Ma presumo che le persone abbiano motivi per chiedere. O anche se l'OP no, altri futuri visitatori potrebbero farlo. Quindi, IMHO, una risposta più un rimprovero è meglio che solo un rimprovero.
arivero

13
@arivero: Questo non ha risposto alla mia domanda.
S.Lott

9
@ S.Lott Penso che dovresti collegare il "mostrato" alla risposta effettiva, non ho davvero un problema con questa è la risposta accettata ma ci sono ragioni per cui devi scimmiottare la patch in alcune circostanze e dalla mia lettura veloce Ho preso "come mostrato" per indicare ciò che stavi mostrando, piuttosto che una risposta diversa.
Daniel Chatfield

4
La sottoclasse è un contratto molto più forte del metodo di rattoppare le scimmie. Quando possibile, contratti forti prevengono comportamenti imprevisti. Ma in alcuni casi, è auspicabile un contratto più flessibile. In quelle situazioni, preferirei usare un callback - invece di monkey-patching - perché, consentendo la personalizzazione di alcuni comportamenti, il contratto di classe rimane inviolato. La tua risposta, sebbene un consiglio pratico, è piuttosto scarsa per questa domanda e il tuo stile di codifica è incoerente.
Aaron3468

169

Si è possibile:

class Dog:
    def bark(self):
        print "Woof"

def new_bark(self):
    print "Woof Woof"

foo = Dog()

funcType = type(Dog.bark)

# "Woof"
foo.bark()

# replace bark with new_bark for this object only
foo.bark = funcType(new_bark, foo, Dog)

foo.bark()
# "Woof Woof"

1
Un piccolo commento, quando si esegue funcType (new_bark, foo, Dog) si aggiunge il nome == bark e il metodo di istanza Dog.new_bark in foo .__ dict__ giusto? Quindi, quando chiami di nuovo, prima cerca nel dizionario delle istanze e richiedi quello,
James

11
Spiega cosa fa, soprattutto cosa funcTypefa e perché è necessario.
Aleksandr Dubinsky

3
Penso che questo possa essere reso un po 'più semplice ed esplicito usando funcType = types.MethodType(dopo l'importazione types) invece di funcType = type(Dog.bark).
Elias Zamaria

13
Immagino che questo non funzioni con Python 3. Ricevo l'errore "TypeError: function () l'argomento 1 deve essere codice, non funzione". Qualche suggerimento per Python 3?
Sait

2
Puoi anche chiamare __get__la funzione per associarla all'istanza.
Mad Physicist

41

È necessario utilizzare MethodType dal typesmodulo. Lo scopo di MethodTypeè quello di sovrascrivere i metodi a livello di istanza (in modo che selfpossano essere disponibili nei metodi sovrascritti).

Vedi l'esempio sotto.

import types

class Dog:
    def bark(self):
        print "WOOF"

boby = Dog()
boby.bark() # WOOF

def _bark(self):
    print "WoOoOoF!!"

boby.bark = types.MethodType(_bark, boby)

boby.bark() # WoOoOoF!!

32

Per spiegare l'eccellente risposta di @ codelogic, propongo un approccio più esplicito. Questa è la stessa tecnica utilizzata .dall'operatore per associare un metodo di classe quando vi si accede come attributo di istanza, tranne per il fatto che il metodo sarà effettivamente una funzione definita al di fuori di una classe.

Lavorando con il codice di @ codelogic, l'unica differenza è nel modo in cui il metodo è associato. Sto usando il fatto che funzioni e metodi non sono descrittori di dati in Python e invoco il __get__metodo. Nota in particolare che sia l'originale che la sostituzione hanno firme identiche, il che significa che puoi scrivere la sostituzione come un metodo di classe completo, accedendo a tutti gli attributi dell'istanza tramite self.

classe Cane:
    def bark (self):
        stampa "Woof"

def new_bark (self):
    stampa "Woof Woof"

foo = Cane ()

# "Woof"
foo.bark ()

# sostituisci bark con new_bark solo per questo oggetto
foo.bark = new_bark .__ get __ (foo, Dog)

foo.bark ()
# "Bau bau"

Assegnando il metodo vincolato a un attributo di istanza, hai creato una simulazione quasi completa dell'override di un metodo. Una caratteristica utile che manca è l'accesso alla versione senza argomenti di super, poiché non sei in una definizione di classe. Un'altra cosa è che l' __name__attributo del tuo metodo associato non prenderà il nome della funzione che sta sovrascrivendo, come nella definizione di classe, ma puoi comunque impostarlo manualmente. La terza differenza è che il tuo metodo associato manualmente è un semplice riferimento di attributo che sembra essere una funzione. L' .operatore non fa altro che recuperare quel riferimento. Quando si richiama un metodo regolare da un'istanza d'altra parte, il processo di associazione crea ogni volta un nuovo metodo associato.

L'unico motivo per cui funziona, a proposito, è che gli attributi di istanza sovrascrivono i descrittori non di dati . I descrittori di dati hanno __set__metodi, che metodi (fortunatamente per te) no. I descrittori di dati nella classe hanno effettivamente la priorità su qualsiasi attributo di istanza. Questo è il motivo per cui puoi assegnare una proprietà: il loro __set__metodo viene richiamato quando provi a fare un'assegnazione. Personalmente mi piace fare un ulteriore passo avanti e nascondere il valore effettivo dell'attributo sottostante nell'istanza __dict__, dove è inaccessibile con mezzi normali proprio perché la proprietà lo ombreggia.

Dovresti anche tenere a mente che questo è inutile per i metodi magici (doppio trattino basso) . I metodi magici possono ovviamente essere sovrascritti in questo modo, ma le operazioni che li usano guardano solo al tipo. Ad esempio, puoi impostare __contains__qualcosa di speciale nella tua istanza, ma la chiamata x in instancelo ignorerebbe e userebbe type(instance).__contains__(instance, x)invece. Questo si applica a tutti i metodi magici specificati nel modello di dati Python .


1
Questa dovrebbe essere la risposta accettata: è pulito e funziona in Python 3.
BlenderBender

@BlenderBender. Apprezzo il tuo supporto
Mad Physicist

Qual è la differenza tra questa risposta e quella sopra la tua di @Harshal Dhumai?
1313e

1
@ 1313. Funzionalmente, non dovrebbe esserci molta differenza. Sospetto che la risposta sopra possa accettare una gamma più ampia di invocabili come input, ma senza leggere i documenti e giocare, non ne sono sicuro.
Mad Physicist

Documenti di Python 2 per questo: docs.python.org/2/howto/descriptor.html#functions-and-methods . Quindi in teoria è la stessa della risposta di @Harshal Dhumai.
Rockallite

26
class Dog:
    def bark(self):
        print "WOOF"

boby = Dog()
boby.bark() # WOOF

# METHOD OVERRIDE
def new_bark():
    print "WoOoOoF!!"
boby.bark = new_bark

boby.bark() # WoOoOoF!!

È possibile utilizzare la bobyvariabile all'interno della funzione, se necessario. Poiché stai sovrascrivendo il metodo solo per questo oggetto istanza, in questo modo è più semplice e ha esattamente lo stesso effetto dell'uso self.


1
IMHO che utilizza la firma originale aumenta la leggibilità, specialmente se la funzione è definita altrove nel codice, non vicino all'istanza. L'eccezione sarebbe il caso in cui il metodo di sostituzione viene utilizzato anche in modo indipendente come funzione. Ovviamente, in questo semplice esempio, non importa.
codelogic

1
Non capisco perché questa non sia la risposta accettata. Si chiama patchinge questo è il modo corretto per farlo (ad esempio boby = Dog()e boby.bark = new_bark). È incredibilmente utile nei test unitari per i controlli. Per ulteriori spiegazioni vedere tryolabs.com/blog/2013/07/05/run-time-method-patching-python (esempi) - no non sono affiliato con il sito collegato o l'autore.
Geoff

5
Il metodo new_bark non ha accesso a self (istanza), quindi non è possibile che l'utente acceda alle proprietà dell'istanza in new_bark. Invece è necessario utilizzare MethodType dal modulo types (vedere la mia risposta sotto).
Harshal Dhumal

3

Poiché nessuno sta menzionando functools.partialqui:

from functools import partial

class Dog:
    name = "aaa"
    def bark(self):
        print("WOOF")

boby = Dog()
boby.bark() # WOOF

def _bark(self):
    print("WoOoOoF!!")

boby.bark = partial(_bark, boby)
boby.bark() # WoOoOoF!!

1

Poiché le funzioni sono oggetti di prima classe in Python, puoi passarle durante l'inizializzazione dell'oggetto classe o sovrascriverlo in qualsiasi momento per una determinata istanza di classe:

class Dog:
    def __init__(self,  barkmethod=None):
        self.bark=self.barkp
        if barkmethod:
           self.bark=barkmethod
    def barkp(self):
        print "woof"

d=Dog()
print "calling original bark"
d.bark()

def barknew():
    print "wooOOOoof"

d1=Dog(barknew)
print "calling the new bark"
d1.bark()

def barknew1():
    print "nowoof"

d1.bark=barknew1
print "calling another new"
d1.bark()

e i risultati sono

calling original bark
woof
calling the new bark
wooOOOoof
calling another new
nowoof

-4

Anche se mi è piaciuta l'idea di ereditarietà di S. Lott e sono d'accordo con la cosa 'tipo (a)', ma poiché anche le funzioni hanno attributi accessibili, penso che possa essere gestita in questo modo:

class Dog:
    def __init__(self, barkmethod=None):
        self.bark=self.barkp
        if barkmethod:
           self.bark=barkmethod
    def barkp(self):
        """original bark"""
        print "woof"

d=Dog()
print "calling original bark"
d.bark()
print "that was %s\n" % d.bark.__doc__

def barknew():
    """a new type of bark"""
    print "wooOOOoof"

d1=Dog(barknew)
print "calling the new bark"
d1.bark()
print "that was %s\n" % d1.bark.__doc__

def barknew1():
    """another type of new bark"""
    print "nowoof"

d1.bark=barknew1
print "another new"
d1.bark()
print "that was %s\n" % d1.bark.__doc__

e l'output è:

calling original bark
woof
that was original bark

calling the new bark
wooOOOoof
that was a new type of bark

another new
nowoof
that was another type of new bark

2
Se ha bisogno di essere "gestito", allora - per me - qualcosa non va. Soprattutto quando c'è una funzionalità linguistica di prima classe che fa già il lavoro.
S.Lott

-4

Caro, questo non sta sovrascrivendo, stai solo chiamando la stessa funzione due volte con l'oggetto. Fondamentalmente l'override è correlato a più di una classe. quando lo stesso metodo di firma esiste in classi diverse, la funzione che stai chiamando decide l'oggetto che la chiama. L'override è possibile in python quando si crea più di una classe, vengono scritte le stesse funzioni e una cosa in più da condividere che il sovraccarico non è consentito in python


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.