Python: associare un metodo non associato?


117

In Python, c'è un modo per associare un metodo non associato senza chiamarlo?

Sto scrivendo un programma wxPython e per una certa classe ho deciso che sarebbe stato bello raggruppare i dati di tutti i miei pulsanti insieme come un elenco di tuple a livello di classe, in questo modo:

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)

Il problema è che, poiché tutti i valori di handlersono metodi non vincolati, il mio programma esplode in un tripudio spettacolare e io piango.

Stavo cercando una soluzione online a quello che sembra dovrebbe essere un problema relativamente semplice e risolvibile. Purtroppo non sono riuscito a trovare niente. In questo momento, sto usando functools.partialper aggirare questo problema, ma qualcuno sa se esiste un modo pulito, sano e pitonico per associare un metodo non associato a un'istanza e continuare a trasmetterlo senza chiamarlo?


6
@Christopher - Un metodo che non è vincolato all'ambito dell'oggetto da cui è stato risucchiato, quindi devi passare esplicitamente self.
Aiden Bell

2
Mi piace particolarmente "fiammata spettacolare e piango".
jspencer

Risposte:


174

Tutte le funzioni sono anche descrittori , quindi puoi associarle chiamando il loro __get__metodo:

bound_handler = handler.__get__(self, MyWidget)

Ecco l'eccellente guida ai descrittori di R. Hettinger .


Come esempio autonomo tratto dal commento di 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

3
È piuttosto interessante. Mi piace come puoi omettere il tipo e ottenere invece un "metodo vincolato? .F".
Kiv

Mi piace questa soluzione rispetto a MethodTypequella, perché funziona allo stesso modo in py3k, mentre MethodTypegli argomenti di sono stati leggermente modificati.
bgw

10
E quindi, una funzione per associare funzioni a istanze di classe: bind = lambda instance, func, asname: setattr(instance, asname, func.__get__(instance, instance.__class__))Esempio:class A: pass; a = A(); bind(a, bind, 'bind')
Keith Pinson

2
Eh, impari qualcosa di nuovo ogni giorno. @Kazark In Python 3, almeno, puoi anche saltare la fornitura del tipo, poiché __get__lo prenderà implicitamente dal parametro object. Non sono nemmeno sicuro che fornirlo faccia qualcosa, in quanto non fa differenza il tipo che fornisco come secondo parametro indipendentemente da quale sia l'istanza del primo parametro. Quindi bind = lambda instance, func, asname=None: setattr(instance, asname or func.__name__, func.__get__(instance))dovrebbe fare anche il trucco. (Anche se preferirei essere bindutilizzabile come decoratore, personalmente, ma questa è una questione diversa.)
JAB

1
Wow, non ho mai saputo che le funzioni fossero descrittori. È un design molto elegante, i metodi sono solo semplici funzioni nella classe __dict__e l'accesso agli attributi ti dà metodi non associati o vincolati attraverso il normale protocollo descrittore. Ho sempre pensato che fosse una sorta di magia accaduta durantetype.__new__()
JaredL

81

Questo può essere fatto in modo pulito con types.MethodType . Esempio:

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

10
+1 Questo è fantastico, ma non c'è alcun riferimento ad esso nei documenti di Python all'URL che hai fornito.
Kyle Wild

6
+1, preferisco non avere chiamate a funzioni magiche nel mio codice (cioè __get__). Non so per quale versione di python l'hai testata, ma su python 3.4, la MethodTypefunzione accetta due argomenti. La funzione e l'istanza. Quindi questo dovrebbe essere cambiato in types.MethodType(f, C()).
Dan Milon

Ecco qui! È un buon modo per applicare patch ai metodi di istanza:wgt.flush = types.MethodType(lambda self: None, wgt)
Winand

2
In realtà è menzionato nei documenti, ma nella pagina del descrittore dall'altra risposta: docs.python.org/3/howto/descriptor.html#functions-and-methods
kai

9

Creare una chiusura con il sé in essa non vincolerà tecnicamente la funzione, ma è un modo alternativo di risolvere lo stesso (o molto simile) problema sottostante. Ecco un esempio banale:

self.method = (lambda self: lambda args: self.do(args))(self)

1
Sì, è più o meno lo stesso della mia correzione originale, che doveva usarefunctools.partial(handler, self)
Dan Passaro

7

Questo si vincolerà selfa handler:

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

Funziona passando selfcome primo argomento alla funzione. object.function()è solo zucchero sintattico per function(object).


1
Sì, ma questo chiama il metodo. Il problema è che devo essere in grado di passare il metodo associato come oggetto richiamabile. Ho il metodo non associato e l'istanza a cui vorrei che fosse associato, ma non riesco a capire come metterlo insieme senza chiamarlo immediatamente
Dan Passaro

9
No, non lo farà, chiamerà il metodo solo se esegui bound_handler (). La definizione di un lambda non chiama il lambda.
brian-brazil

1
Potresti effettivamente usare functools.partialinvece di definire un lambda. Tuttavia, non risolve il problema esatto. Hai ancora a che fare con un functioninvece di un instancemethod.
Alan Plum

5
@Alan: qual è la differenza tra un il functioncui primo argomento hai parzialmente modificato e instancemethod; digitando anatra non può vedere la differenza.
Lie Ryan

2
@LieRyan la differenza è che non hai ancora a che fare con il tipo fondamentale. functools.partialrilascia alcuni metadati, ad es __module__. (Voglio anche affermare per la cronaca che rabbrividisco molto quando guardo il mio primo commento su questa risposta.) In effetti nella mia domanda menziono che sto già usando functools.partialma sentivo che doveva esserci un modo "più puro", poiché è facile ottenere metodi non associati e vincolati.
Dan Passaro

1

In ritardo alla festa, ma sono venuto qui con una domanda simile: ho un metodo di classe e un'istanza e voglio applicare l'istanza al metodo.

A rischio di semplificare eccessivamente la domanda dell'OP, ho finito per fare qualcosa di meno misterioso che potrebbe essere utile ad altri che arrivano qui (avvertimento: sto lavorando in Python 3 - YMMV).

Considera questa semplice classe:

class Foo(object):

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

    def value(self):
        return self._value

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

Ecco cosa puoi fare con esso:

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

Questo non risolve il mio problema - che è che volevo methessere invocabile senza dovergli inviare l' aargomento (motivo per cui inizialmente l'ho usato functools.partial) - ma questo è preferibile se non hai bisogno di passare il metodo e puoi semplicemente invocarlo sul posto. Anche questo funziona allo stesso modo in Python 2 come in Python 3.
Dan Passaro

Ci scusiamo per non aver letto i requisiti originali con maggiore attenzione. Sono parziale (gioco di parole) all'approccio basato su lambda fornito da @ brian-brazil in stackoverflow.com/a/1015355/558639 - è il più puro possibile.
fearless_fool
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.