Python: lier une méthode non liée?

117

En Python, existe-t-il un moyen de lier une méthode indépendante sans l'appeler?

J'écris un programme wxPython, et pour une certaine classe, j'ai décidé que ce serait bien de regrouper les données de tous mes boutons sous forme de liste de tuples au niveau de la classe, comme ceci:

class MyWidget(wx.Window):
    buttons = [("OK", OnOK),
               ("Cancel", OnCancel)]

    # ...

    def Setup(self):
        for text, handler in MyWidget.buttons:

            # This following line is the problem line.
            b = wx.Button(parent, label=text).Bind(wx.EVT_BUTTON, handler)

Le problème est que, puisque toutes les valeurs de handlersont des méthodes non liées, mon programme explose dans une flambée spectaculaire et je pleure.

Je cherchais en ligne une solution à ce qui semble être un problème relativement simple et résoluble. Malheureusement, je n'ai rien trouvé. En ce moment, j'utilise functools.partialpour contourner ce problème, mais est-ce que quelqu'un sait s'il existe une manière pythonique, saine et propre de lier une méthode non liée à une instance et de continuer à la transmettre sans l'appeler?

Dan Passaro
la source
6
@Christopher - Une méthode qui n'est pas liée à la portée de l'objet dont elle a été aspirée, vous devez donc passer explicitement self.
Aiden Bell
2
J'aime particulièrement «l'incendie spectaculaire et je pleure».
jspencer

Réponses:

174

Toutes les fonctions sont également des descripteurs , vous pouvez donc les lier en appelant leur __get__méthode:

bound_handler = handler.__get__(self, MyWidget)

Voici l'excellent guide des descripteurs de R. Hettinger .


En tant qu'exemple autonome tiré du commentaire de Keith :

def bind(instance, func, as_name=None):
    """
    Bind the function *func* to *instance*, with either provided name *as_name*
    or the existing name of *func*. The provided *func* should accept the 
    instance as the first argument, i.e. "self".
    """
    if as_name is None:
        as_name = func.__name__
    bound_method = func.__get__(instance, instance.__class__)
    setattr(instance, as_name, bound_method)
    return bound_method

class Thing:
    def __init__(self, val):
        self.val = val

something = Thing(21)

def double(self):
    return 2 * self.val

bind(something, double)
something.double()  # returns 42
Alex Martelli
la source
3
C'est plutôt cool. J'aime comment vous pouvez omettre le type et récupérer une "méthode liée? .F" à la place.
Kiv
J'aime cette solution plutôt que MethodTypecelle, car elle fonctionne de la même façon dans py3k, alors que MethodTypeles arguments de ont été légèrement modifiés.
bgw
10
Et donc, une fonction pour lier des fonctions à des instances de classe: bind = lambda instance, func, asname: setattr(instance, asname, func.__get__(instance, instance.__class__))Exemple:class A: pass; a = A(); bind(a, bind, 'bind')
Keith Pinson
2
Huh, tu apprends quelque chose de nouveau chaque jour. @Kazark Dans Python 3, au moins, vous pouvez également ignorer la fourniture du type, comme __get__cela prendra implicitement le paramètre object. Je ne sais même pas si le fournir fait quelque chose, car le type que je fournis en tant que deuxième paramètre ne fait aucune différence, quel que soit le premier paramètre. Alors bind = lambda instance, func, asname=None: setattr(instance, asname or func.__name__, func.__get__(instance))devrait faire l'affaire aussi. (Bien que je préfère avoir bindutilisable en tant que décorateur, personnellement, mais c'est une autre affaire.)
JAB
1
Wow, je n'ai jamais su que les fonctions étaient des descripteurs. C'est une conception très élégante, les méthodes ne sont que des fonctions simples dans la classe __dict__et l'accès aux attributs vous donne des méthodes non liées ou liées via le protocole de descripteur normal. J'ai toujours supposé que c'était une sorte de magie qui s'était produite pendanttype.__new__()
JaredL
81

Cela peut être fait proprement avec types.MethodType . Exemple:

import types

def f(self): print self

class C(object): pass

meth = types.MethodType(f, C(), C) # Bind f to an instance of C
print meth # prints <bound method C.f of <__main__.C object at 0x01255E90>>
Kiv
la source
10
+1 C'est génial, mais il n'y a aucune référence à cela dans la documentation python à l'URL que vous avez fournie.
Kyle Wild
6
+1, je préfère ne pas avoir d'appels à des fonctions magiques dans mon code (ie __get__). Je ne sais pas pour quelle version de python vous l'avez testé, mais sur python 3.4, la MethodTypefonction prend deux arguments. La fonction et l'instance. Cela devrait donc être changé en types.MethodType(f, C()).
Dan Milon
C'est ici! C'est un bon moyen de patcher les méthodes d'instance:wgt.flush = types.MethodType(lambda self: None, wgt)
Winand
2
Il est en fait mentionné dans la documentation, mais dans la page de descripteur de l'autre réponse: docs.python.org/3/howto/descriptor.html#functions-and-methods
kai
9

Créer une fermeture avec soi-même ne liera pas techniquement la fonction, mais c'est une manière alternative de résoudre le même problème sous-jacent (ou très similaire). Voici un exemple trivial:

self.method = (lambda self: lambda args: self.do(args))(self)
Keith Pinson
la source
1
Oui, c'est à peu près le même que mon correctif d'origine, qui devait être utiliséfunctools.partial(handler, self)
Dan Passaro
7

Cela se liera selfà handler:

bound_handler = lambda *args, **kwargs: handler(self, *args, **kwargs)

Cela fonctionne en passant selfcomme premier argument à la fonction. object.function()est juste du sucre syntaxique pour function(object).

brian-brésil
la source
1
Oui, mais cela appelle la méthode. Le problème est que je dois pouvoir passer la méthode liée en tant qu'objet appelable. J'ai la méthode non liée et l'instance à laquelle je voudrais qu'elle soit liée, mais je ne peux pas comprendre comment mettre tout cela ensemble sans l'appeler immédiatement
Dan Passaro
9
Non, cela n'appellera la méthode que si vous faites bound_handler (). La définition d'un lambda n'appelle pas le lambda.
brian-brazil
1
Vous pouvez en fait utiliser functools.partialau lieu de définir un lambda. Cela ne résout pas le problème exact, cependant. Vous avez toujours affaire à un functionau lieu d'un instancemethod.
Alan Plum
5
@Alan: quelle est la différence entre un functiondont le premier argument a été partiel et instancemethod; la frappe de canard ne peut pas voir la différence.
Lie Ryan
2
@LieRyan, la différence est que vous n'avez toujours pas affaire au type fondamental. functools.partialsupprime certaines métadonnées, par exemple __module__. (Aussi je veux dire pour l'enregistrement que je grince des dents quand je regarde mon premier commentaire sur cette réponse.) En fait, dans ma question, je mentionne que j'utilise déjà functools.partialmais j'ai senti qu'il devait y avoir un moyen plus "pur", car il est facile d'obtenir à la fois des méthodes non liées et des méthodes liées.
Dan Passaro
1

En retard à la fête, mais je suis venu ici avec une question similaire: j'ai une méthode de classe et une instance, et je veux appliquer l'instance à la méthode.

Au risque de trop simplifier la question de l'OP, j'ai fini par faire quelque chose de moins mystérieux qui pourrait être utile à d'autres qui arrivent ici (mise en garde: je travaille en Python 3 - YMMV).

Considérez cette classe simple:

class Foo(object):

    def __init__(self, value):
        self._value = value

    def value(self):
        return self._value

    def set_value(self, value):
        self._value = value

Voici ce que vous pouvez en faire:

>>> meth = Foo.set_value   # the method
>>> a = Foo(12)            # a is an instance with value 12
>>> meth(a, 33)            # apply instance and method
>>> a.value()              # voila - the method was called
33
sans peur
la source
Cela ne résout pas mon problème - qui est que je voulais methêtre invocable sans avoir à lui envoyer l' aargument (c'est pourquoi j'ai initialement utilisé functools.partial) - mais c'est préférable si vous n'avez pas besoin de passer la méthode et que vous pouvez simplement invoquez-le sur place. De plus, cela fonctionne de la même manière dans Python 2 que dans Python 3.
Dan Passaro
Toutes nos excuses pour ne pas avoir lu plus attentivement vos exigences originales. Je suis partisan (jeu de mots) de l'approche lambda donnée par @ brian-brazil dans stackoverflow.com/a/1015355/558639 - elle est à peu près aussi pure que possible.
peurless_fool