Python: Bind an Unbound Method?

117

Czy w Pythonie istnieje sposób na powiązanie niezwiązanej metody bez jej wywoływania?

Piszę program wxPython i dla pewnej klasy zdecydowałem, że fajnie byłoby zgrupować dane wszystkich moich przycisków razem jako listę krotek na poziomie klasy, na przykład:

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)

Problem w tym, że ponieważ wszystkie wartości handlersą niezwiązane z metodami, mój program wybucha spektakularnym płomieniem i płaczę.

Szukałem w Internecie rozwiązania tego, co wydaje się być stosunkowo prostym, możliwym do rozwiązania problemem. Niestety nic nie mogłem znaleźć. W tej chwili używam functools.partialdo obejścia tego problemu, ale czy ktoś wie, czy istnieje czysty, zdrowy, Pythonowy sposób powiązania niezwiązanej metody z instancją i kontynuowania przekazywania jej bez wywoływania jej?

Dan Passaro
źródło
6
@Christopher - Metoda, która nie jest związana z zakresem obiektu, z którego została wyssana, więc musisz jawnie przekazać self.
Aiden Bell
2
Szczególnie podoba mi się „spektakularny ogień i płaczę”.
jspencer

Odpowiedzi:

174

Wszystkie funkcje są również deskryptorami , więc możesz je powiązać, wywołując ich __get__metodę:

bound_handler = handler.__get__(self, MyWidget)

Oto doskonały przewodnik R. Hettingera po deskryptorach.


Jako samodzielny przykład zaczerpnięty z komentarza Keitha :

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
źródło
3
To fajnie. Podoba mi się sposób, w jaki można pominąć typ i zamiast tego uzyskać „powiązaną metodę? .F”.
Kiv
To rozwiązanie podoba mi się bardziej niż MethodTypejedno, ponieważ działa tak samo w py3k, a MethodTypeargumenty zostały nieco zmienione.
bgw
10
A zatem funkcja do wiązania funkcji z instancjami klas: bind = lambda instance, func, asname: setattr(instance, asname, func.__get__(instance, instance.__class__))Przykład:class A: pass; a = A(); bind(a, bind, 'bind')
Keith Pinson
2
Huh, każdego dnia uczysz się czegoś nowego. @Kazark Przynajmniej w Pythonie 3 możesz pominąć podawanie typu, ponieważ __get__zostanie to niejawnie uwzględnione w parametrze obiektu. Nie jestem nawet pewien, czy dostarczenie tego spowoduje cokolwiek, ponieważ nie ma znaczenia, jaki typ podam jako drugi parametr, niezależnie od tego, którego wystąpieniem jest pierwszy parametr. Więc też bind = lambda instance, func, asname=None: setattr(instance, asname or func.__name__, func.__get__(instance))powinien załatwić sprawę. (Chociaż wolałbym mieć bindużyteczne jako dekorator, osobiście, ale to inna sprawa.)
JAB
1
Wow, nigdy nie wiedziałem, że funkcje są deskryptorami. To bardzo elegancki projekt, metody są po prostu zwykłymi funkcjami w klasie, __dict__a dostęp do atrybutów zapewnia niezwiązane lub powiązane metody przez normalny protokół deskryptora. Zawsze zakładałem, że to jakaś magia wydarzyła się podczastype.__new__()
JaredL,
81

Można to zrobić czysto za pomocą types.MethodType . Przykład:

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
źródło
10
+1 To jest niesamowite, ale nie ma do niego odniesienia w dokumentach Pythona pod podanym adresem URL.
Kyle Wild
7
+1, wolę nie mieć wywołań funkcji magicznych w moim kodzie (tj __get__.). Nie wiem, dla której wersji Pythona to przetestowałeś, ale w Pythonie 3.4 MethodTypefunkcja przyjmuje dwa argumenty. Funkcja i instancja. Więc to powinno zostać zmienione na types.MethodType(f, C()).
Dan Milon
Oto jest! To dobry sposób na łatanie metod instancji:wgt.flush = types.MethodType(lambda self: None, wgt)
Winand
2
Właściwie jest to wspomniane w dokumentacji, ale na stronie deskryptora z drugiej odpowiedzi: docs.python.org/3/howto/descriptor.html#functions-and-methods
kai
9

Utworzenie domknięcia zawierającego siebie nie będzie technicznie wiązało funkcji, ale jest alternatywnym sposobem rozwiązania tego samego (lub bardzo podobnego) podstawowego problemu. Oto trywialny przykład:

self.method = (lambda self: lambda args: self.do(args))(self)
Keith Pinson
źródło
1
Tak, to mniej więcej to samo, co moja oryginalna poprawka, z której miałem korzystaćfunctools.partial(handler, self)
Dan Passaro
7

Spowoduje to powiązanie selfz handler:

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

Działa to przekazując selfjako pierwszy argument funkcji. object.function()jest tylko cukrem syntaktycznym function(object).

brian-brazil
źródło
1
Tak, ale to wywołuje metodę. Problem polega na tym, że muszę móc przekazać powiązaną metodę jako obiekt wywoływalny. Mam metodę niezwiązaną i instancję, do której chciałbym, aby była związana, ale nie mogę wymyślić, jak to wszystko połączyć, bez natychmiastowego wywołania tego
Dan Passaro
9
Nie, nie, wywoła metodę tylko wtedy, gdy wykonasz bound_handler (). Definiowanie lambdy nie wywołuje lambdy.
brian-brazil
1
Właściwie możesz użyć functools.partialzamiast definiować lambdę. Nie rozwiązuje to jednak dokładnego problemu. Nadal masz do czynienia z functionzamiast instancemethod.
Alan Plum
5
@Alan: jaka jest różnica między a, functionktórego pierwszy argument częściowo rozwiązałeś, a instancemethod; pisanie kaczką nie widzi różnicy.
Lie Ryan
2
@LieRyan różnica polega na tym, że nadal nie masz do czynienia z typem podstawowym. functools.partialporzuca niektóre metadane, np __module__. (Chciałbym również zaznaczyć, że bardzo się wzdrygam, kiedy patrzę na mój pierwszy komentarz do tej odpowiedzi.) W rzeczywistości w moim pytaniu wspominam, że już używam, functools.partialale czułem, że musi istnieć „czystszy” sposób, ponieważ łatwo jest uzyskać zarówno metody niezwiązane, jak i powiązane.
Dan Passaro,
1

Spóźniłem się na imprezę, ale przyszedłem tutaj z podobnym pytaniem: mam metodę klasową i instancję i chcę zastosować instancję do metody.

Ryzykując nadmierne uproszczenie pytania OP, skończyło się na zrobieniu czegoś mniej tajemniczego, co może być przydatne dla innych, którzy tu przyjeżdżają (uwaga: pracuję w Pythonie 3 - YMMV).

Rozważ tę prostą klasę:

class Foo(object):

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

    def value(self):
        return self._value

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

Oto, co możesz z tym zrobić:

>>> 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
nieustraszony głupiec
źródło
To nie rozwiązuje mojego problemu - polegającego na tym, że chciałem methbyć wywoływalny bez konieczności wysyłania aargumentu (dlatego początkowo użyłem functools.partial) - ale jest to lepsze, jeśli nie musisz przekazywać metody i możesz po prostu wywołaj go na miejscu. Działa to również w ten sam sposób w Pythonie 2, jak w Pythonie 3.
Dan Passaro
Przepraszamy za niedokładne przeczytanie oryginalnych wymagań. Jestem częściowo (gra słów zamierzona) w podejściu opartym na lambdzie podanym przez @ brian-brazil na stackoverflow.com/a/1015355/558639 - jest tak czysty, jak tylko możesz.
Foolless_fool