Remplacer une méthode au niveau de l'instance

87

Existe-t-il un moyen en Python de remplacer une méthode de classe au niveau de l'instance? Par exemple:

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

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

Réponses:

11

Veuillez ne pas faire cela comme indiqué. Votre code devient illisible lorsque vous identifiez une instance pour qu'elle soit différente de la classe.

Vous ne pouvez pas déboguer le code monkeypatched.

Lorsque vous trouvez un bogue dans bobyet print type(boby), vous verrez que (a) c'est un chien, mais (b) pour une raison obscure, il n'aboie pas correctement. C'est un cauchemar. Ne fais pas ça.

Veuillez faire ceci à la place.

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!!
S.Lott
la source
9
@arivero: Je pensais que le "Veuillez ne pas faire ceci comme indiqué" le rendait parfaitement clair. Quels autres mots aimeriez-vous voir pour préciser que cela ne répond pas à la question qui a été posée, mais donne des conseils sur les raisons pour lesquelles c'est une mauvaise idée?
S.Lott
45
Je ne suis pas en désaccord sur les conseils, ni sur le PO qu'il n'y paraît. Mais je suppose que les gens ont des raisons de demander. Ou même si l'OP ne l'a pas fait, d'autres futurs visiteurs pourraient le faire. Donc, à mon humble avis, une réponse plus une réprimande vaut mieux qu'une réprimande.
arivero
13
@arivero: Cela n'a pas répondu à ma question.
S.Lott
9
@ S.Lott Je pense que vous devriez lier le "montré" à la réponse réelle, je n'ai pas vraiment de problème à ce que cette réponse soit acceptée, mais il y a des raisons pour lesquelles vous avez besoin de monkey patch dans certaines circonstances et d'après ma lecture rapide J'ai pris «comme indiqué» pour signifier ce que vous montriez, plutôt qu'une réponse différente.
Daniel Chatfield
4
Le sous-classement est un contrat beaucoup plus fort que le simple patching d'une méthode. Lorsque cela est possible, des contrats solides empêchent les comportements inattendus. Mais dans certains cas, un contrat plus souple est souhaitable. Dans ces situations, je préférerais utiliser un callback - au lieu de monkey-patching - car, en permettant à certains comportements d'être personnalisés, le contrat de classe reste intact. Votre réponse, bien que des conseils pratiques, est assez pauvre pour cette question et votre style de codage est incohérent.
Aaron3468
166

Oui c'est possible:

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"
codelogic
la source
1
Un petit commentaire, quand vous faites funcType (new_bark, foo, Dog), il ajoute le nom == bark et la méthode d'instance Dog.new_bark dans foo .__ dict__ n'est-ce pas? Donc, lorsque vous appelez à nouveau, recherchez d'abord dans le dictionnaire d'instances et appelez pour cela,
James
11
Veuillez expliquer ce que cela fait, en particulier ce que funcTypefait et pourquoi cela est nécessaire.
Aleksandr Dubinsky
3
Je pense que cela peut être rendu un peu plus simple et plus explicite en utilisant funcType = types.MethodType(après l'importation types) au lieu de funcType = type(Dog.bark).
Elias Zamaria
13
Je suppose que cela ne fonctionne pas avec Python 3. J'obtiens une erreur "TypeError: function () argument 1 must be code, not function". Des suggestions pour Python 3?
Sait
2
Vous pouvez également appeler __get__la fonction pour la lier à l'instance.
Mad Physicist
37

Vous devez utiliser MethodType à partir du typesmodule. Le but de MethodTypeest d'écraser les méthodes au niveau de l'instance (afin qu'elles selfpuissent être disponibles dans les méthodes écrasées).

Voir l'exemple ci-dessous.

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!!
Harshal Dhumal
la source
31

Pour expliquer l'excellente réponse de @ codelogic, je propose une approche plus explicite. C'est la même technique que l' .opérateur approfondit pour lier une méthode de classe lorsque vous y accédez en tant qu'attribut d'instance, sauf que votre méthode sera en fait une fonction définie en dehors d'une classe.

En travaillant avec le code de @ codelogic, la seule différence réside dans la manière dont la méthode est liée. J'utilise le fait que les fonctions et les méthodes ne sont pas des descripteurs de données en Python et j'appelle la __get__méthode. Notez en particulier que l'original et le remplacement ont des signatures identiques, ce qui signifie que vous pouvez écrire le remplacement en tant que méthode de classe complète, en accédant à tous les attributs d'instance via self.

Chien de classe:
    def bark (self):
        imprimer "Woof"

def new_bark (soi):
    imprimer "Woof Woof"

foo = Chien ()

# "Woof"
foo.bark ()

# remplace bark par new_bark pour cet objet uniquement
foo.bark = new_bark .__ get __ (toto, chien)

foo.bark ()
# "Woof Woof"

En affectant la méthode liée à un attribut d'instance, vous avez créé une simulation presque complète du remplacement d'une méthode. Une fonctionnalité pratique qui manque est l'accès à la version sans argument de super, puisque vous n'êtes pas dans une définition de classe. Une autre chose est que l' __name__attribut de votre méthode liée ne prendra pas le nom de la fonction qu'il remplace, comme il le ferait dans la définition de classe, mais vous pouvez toujours le définir manuellement. La troisième différence est que votre méthode liée manuellement est une simple référence d'attribut qui se trouve être une fonction. L' .opérateur ne fait rien d'autre que chercher cette référence. Lors de l'appel d'une méthode régulière à partir d'une instance, le processus de liaison crée une nouvelle méthode liée à chaque fois.

Soit dit en passant, la seule raison pour laquelle cela fonctionne est que les attributs d'instance remplacent les descripteurs sans données . Les descripteurs de données ont des __set__méthodes, ce qui n'est pas le cas (heureusement pour vous). Les descripteurs de données de la classe ont en fait priorité sur tous les attributs d'instance. C'est pourquoi vous pouvez affecter à une propriété: leur __set__méthode est appelée lorsque vous essayez de faire une affectation. Personnellement, j'aime aller plus loin et cacher la valeur réelle de l'attribut sous-jacent dans l'instance __dict__, où il est inaccessible par des moyens normaux exactement parce que la propriété l'ombrage.

Vous devez également garder à l'esprit que cela est inutile pour les méthodes magiques (double trait de soulignement) . Les méthodes magiques peuvent bien sûr être écrasées de cette manière, mais les opérations qui les utilisent ne regardent que le type. Par exemple, vous pouvez définir __contains__quelque chose de spécial dans votre instance, mais l'appel x in instancene tiendrait pas compte de cela et utiliserait à la type(instance).__contains__(instance, x)place. Cela s'applique à toutes les méthodes magiques spécifiées dans le modèle de données Python .

Physicien fou
la source
1
Cela devrait être la réponse acceptée: c'est propre et fonctionne en Python 3.
BlenderBender
@BlenderBender. J'apprécie votre soutien
Mad Physicist
Quelle est la différence entre cette réponse et celle ci-dessus de la vôtre de @Harshal Dhumai?
1313e
1
@ 1313. Sur le plan fonctionnel, il ne devrait pas y avoir beaucoup de différence. Je soupçonne que la réponse ci-dessus peut accepter une plus large gamme de callables en entrée, mais sans lire la documentation et jouer, je ne suis pas sûr.
Mad Physicist
Documentation Python 2 pour cela: docs.python.org/2/howto/descriptor.html#functions-and-methods . Donc, théoriquement, c'est la même chose que la réponse de @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!!

Vous pouvez utiliser la bobyvariable à l'intérieur de la fonction si vous en avez besoin. Puisque vous remplacez la méthode uniquement pour cet objet d'instance, cette méthode est plus simple et a exactement le même effet que l'utilisation self.

nosklo
la source
1
IMHO utilisant la signature originale ajoute à la lisibilité, surtout si la fonction est définie ailleurs dans le code, pas près de l'instance. L'exception serait le cas où la méthode de substitution est également utilisée indépendamment en tant que fonction. Bien sûr, dans cet exemple simple, cela n'a pas d'importance.
codelogic
1
Je ne comprends pas pourquoi ce n'est pas la réponse acceptée. Il est appelé patchinget c'est la bonne façon de le faire (par exemple boby = Dog()et boby.bark = new_bark). Il est incroyablement utile dans les tests unitaires des contrôles. Pour plus d'explications, voir tryolabs.com/blog/2013/07/05/run-time-method-patching-python (exemples) - non, je ne suis pas affilié au site ou à l'auteur lié.
Geoff
5
La méthode new_bark n'a pas accès à self (instance), il n'y a donc aucun moyen pour l'utilisateur d'accéder aux propriétés de l'instance dans new_bark. Au lieu de cela, il faut utiliser MethodType du module types (voir ma réponse ci-dessous).
Harshal Dhumal
3

Puisque personne ne mentionne functools.partialici:

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!!
user1285245
la source
1

Étant donné que les fonctions sont des objets de première classe en Python, vous pouvez les transmettre lors de l'initialisation de votre objet de classe ou le remplacer à tout moment pour une instance de classe donnée:

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()

et les résultats sont

calling original bark
woof
calling the new bark
wooOOOoof
calling another new
nowoof
JV.
la source
-4

Bien que j'aie aimé l'idée d'héritage de S. Lott et que je sois d'accord avec le 'type (a)', mais comme les fonctions ont aussi des attributs accessibles, je pense que cela peut être géré de cette façon:

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__

et la sortie est:

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
JV.
la source
2
Si cela doit être «géré», alors - pour moi - quelque chose ne va pas. Surtout quand il existe une fonctionnalité de langage de première classe qui fait déjà le travail.
S.Lott
-4

Cher, cela ne remplace pas, vous appelez simplement la même fonction deux fois avec l'objet. Fondamentalement, le remplacement est lié à plusieurs classes. lorsque la même méthode de signature existe dans différentes classes, la fonction que vous appelez décide de l'objet qui l'appelle. Le remplacement est possible en python lorsque vous créez plusieurs classes qui écrivent les mêmes fonctions et une chose de plus à partager que la surcharge n'est pas autorisée en python

nagimsit
la source
-4

J'ai trouvé que c'était la réponse la plus précise à la question initiale

https://stackoverflow.com/a/10829381/7640677

import a

def _new_print_message(message):
    print "NEW:", message

a.print_message = _new_print_message

import b
b.execute()
Ruvalcaba
la source