Python: vincular um método não vinculado?


117

Em Python, existe uma maneira de vincular um método não vinculado sem chamá-lo?

Estou escrevendo um programa wxPython e, para uma determinada classe, decidi que seria bom agrupar os dados de todos os meus botões como uma lista de tuplas em nível de classe, assim:

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)

O problema é que, como todos os valores de handlersão métodos desvinculados, meu programa explode em um incêndio espetacular e eu choro.

Eu estava procurando online por uma solução para o que parece ser um problema relativamente simples e solucionável. Infelizmente não consegui encontrar nada. No momento, estou usando functools.partialpara contornar isso, mas alguém sabe se existe uma maneira Pythônica com sensação de limpeza, saudável, de vincular um método não acoplado a uma instância e continuar passando-o sem chamá-lo?


6
@Christopher - Um método que não está vinculado ao escopo do objeto do qual foi sugado, então você tem que passar self explicitamente.
Aiden Bell de

2
Eu particularmente gosto de "chamas espetaculares e eu choro".
jspencer

Respostas:


174

Todas as funções também são descritores , portanto, você pode vinculá-las chamando seu __get__método:

bound_handler = handler.__get__(self, MyWidget)

Aqui está o excelente guia de descritores de R. Hettinger .


Como um exemplo independente retirado do comentário 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

3
Isso é bem legal. Gosto de como você pode omitir o tipo e, em vez disso, obter um "método vinculado? .F".
Kiv

Eu gosto dessa solução em vez da MethodTypeoutra, porque ela funciona da mesma forma em py3k, enquanto MethodTypeos argumentos de foram um pouco alterados.
bgw

10
E, portanto, uma função para vincular funções a instâncias de classe: bind = lambda instance, func, asname: setattr(instance, asname, func.__get__(instance, instance.__class__))Exemplo:class A: pass; a = A(); bind(a, bind, 'bind')
Keith Pinson

2
Huh, você aprende algo novo todos os dias. @Kazark No Python 3, pelo menos, você também pode pular o fornecimento do tipo, pois __get__irá tirar isso implicitamente do parâmetro do objeto. Eu nem tenho certeza se fornecê-lo faz alguma coisa, já que não faz diferença o tipo que eu forneço como o segundo parâmetro, independentemente do que o primeiro parâmetro é uma instância. Então, bind = lambda instance, func, asname=None: setattr(instance, asname or func.__name__, func.__get__(instance))deve fazer o truque também. (Embora eu prefira bindutilizá-lo como decorador, pessoalmente, mas isso é uma questão diferente.)
JAB

1
Uau, nunca soube que funções eram descritores. Esse é um design muito elegante, os métodos são apenas funções simples na classe __dict__e o acesso ao atributo fornece métodos não acoplados ou vinculados por meio do protocolo descritor normal. Sempre achei que era algum tipo de mágica que aconteceu durantetype.__new__()
JaredL

81

Isso pode ser feito de forma limpa com types.MethodType . Exemplo:

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 Isso é incrível, mas não há referência a ele nos documentos do python no URL que você forneceu.
Kyle Wild

6
1, eu prefiro não ter chamadas para funções mágicas em meu código (ou seja __get__). Não sei em qual versão do python você testou, mas no python 3.4, a MethodTypefunção leva dois argumentos. A função e a instância. Portanto, deve ser alterado para types.MethodType(f, C()).
Dan Milon,

Aqui está! É uma boa maneira de corrigir métodos de instância:wgt.flush = types.MethodType(lambda self: None, wgt)
Winand em

2
Na verdade, ele é mencionado na documentação, mas na página do descritor da outra resposta: docs.python.org/3/howto/descriptor.html#functions-and-methods
kai

9

Criar um fechamento com self nele não vinculará tecnicamente a função, mas é uma maneira alternativa de resolver o mesmo (ou muito semelhante) problema subjacente. Aqui está um exemplo trivial:

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

1
Sim, é quase igual à minha correção original, que era para usarfunctools.partial(handler, self)
Dan Passaro

7

Isso se ligará selfa handler:

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

Isso funciona passando selfcomo o primeiro argumento para a função. object.function()é apenas açúcar sintático para function(object).


1
Sim, mas isso chama o método. O problema é que preciso ser capaz de passar o método vinculado como um objeto chamável. Eu tenho o método não vinculado e a instância que gostaria que ele fosse vinculado, mas não consigo descobrir como colocar tudo junto sem chamá-lo imediatamente
Dan Passaro

9
Não, ele só chamará o método se você fizer bound_handler (). Definir um lambda não chama o lambda.
brian-brazil

1
Você poderia realmente usar em functools.partialvez de definir um lambda. Porém, isso não resolve exatamente o problema. Você ainda está lidando com um em functionvez de um instancemethod.
Alan Plum

5
@Alan: qual é a diferença entre um functioncujo primeiro argumento você parcial-ed e instancemethod; a digitação de pato não consegue ver a diferença.
Lie Ryan

2
@LieRyan, a diferença é que você ainda não está lidando com o tipo fundamental. functools.partialdescarta alguns metadados, por exemplo __module__. (Também quero deixar registrado que me encolho muito quando vejo meu primeiro comentário sobre esta resposta.) Na verdade, em minha pergunta, mencionei que já estou usando, functools.partialmas senti que deveria haver uma maneira "mais pura", uma vez que é fácil obter métodos não associados e associados.
Dan Passaro,

1

Atrasado para a festa, mas cheguei aqui com uma pergunta semelhante: Tenho um método de classe e uma instância e quero aplicar a instância ao método.

Correndo o risco de simplificar demais a pergunta do OP, acabei fazendo algo menos misterioso que pode ser útil para outras pessoas que chegam aqui (ressalva: estou trabalhando em Python 3 - YMMV).

Considere esta classe simples:

class Foo(object):

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

    def value(self):
        return self._value

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

Aqui está o que você pode fazer com ele:

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

Isso não resolve meu problema - que eu queria methser invokable sem ter que enviar o aargumento (que é o motivo pelo qual usei inicialmente functools.partial) - mas isso é preferível se você não precisar passar o método e puder apenas invocá-lo no local. Além disso, isso funciona da mesma maneira no Python 2 e no Python 3.
Dan Passaro

Pedimos desculpas por não ler seus requisitos originais com mais cuidado. Eu sou parcial (trocadilho intencional) para a abordagem baseada em lambda dada por @ brian-brazil em stackoverflow.com/a/1015355/558639 - é o mais puro que você pode imaginar.
fearless_fool
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.