Python: привязать несвязанный метод?

118

В Python есть ли способ привязать несвязанный метод без его вызова?

Я пишу программу wxPython, и для определенного класса я решил, что было бы неплохо сгруппировать данные всех моих кнопок вместе в виде списка кортежей на уровне класса, например:

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)

Проблема в том, что поскольку все значения handlerявляются несвязанными методами, моя программа взрывается ярким пламенем, и я плачу.

Я искал в Интернете решение, которое, казалось, должно быть относительно простой и решаемой проблемой. К сожалению, я ничего не нашел. Прямо сейчас я использую, functools.partialчтобы обойти это, но знает ли кто-нибудь, есть ли чистый, здоровый, питонический способ привязать несвязанный метод к экземпляру и продолжать передавать его, не вызывая его?

Дэн Пассаро
источник
6
@Christopher - метод, который не привязан к области видимости объекта, из которого он был высосан, поэтому вам нужно явно передать self.
Эйден Белл,
2
Мне особенно нравится «эффектное пламя, и я плачу».
jspencer

Ответы:

175

Все функции также являются дескрипторами , поэтому вы можете связать их, вызвав их __get__метод:

bound_handler = handler.__get__(self, MyWidget)

Вот отличное руководство по дескрипторам Р. Хеттингера .


В качестве автономного примера, взятого из комментария Кита :

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
Это круто. Мне нравится, как вы можете опустить тип и вместо этого получить "связанный метод? .F".
Kiv
Мне это решение нравится больше, чем MethodTypeодно, потому что оно работает так же в py3k, а MethodTypeаргументы были немного изменены.
bgw 08
10
И, таким образом, функция для привязки функций к экземплярам класса: bind = lambda instance, func, asname: setattr(instance, asname, func.__get__(instance, instance.__class__))Пример:class A: pass; a = A(); bind(a, bind, 'bind')
Кейт Пинсон,
2
Ха, ты узнаешь что-то новое каждый день. @Kazark В Python 3, по крайней мере, вы также можете пропустить указание типа, так как это __get__будет неявно брать из параметра объекта. Я даже не уверен, что это что-то дает, так как не имеет значения, какой тип я использую в качестве второго параметра, независимо от того, экземпляром какого первого параметра является. Так bind = lambda instance, func, asname=None: setattr(instance, asname or func.__name__, func.__get__(instance))что тоже должно сработать. (Хотя bindлично я бы предпочел, чтобы его можно было использовать в качестве декоратора, но это другой вопрос.)
JAB
1
Вау, никогда не знал, что функции - это дескрипторы. Это очень элегантный дизайн, методы - это просто простые функции в классе, __dict__а доступ к атрибутам дает вам несвязанные или связанные методы через обычный протокол дескриптора. Я всегда предполагал , что это была какая - то магия , что произошло во времяtype.__new__()
JaredL
81

Это можно сделать чисто с помощью types.MethodType . Пример:

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
источник
10
+1 Это круто, но в документах Python по указанному вами URL нет упоминания об этом.
Кайл Уайлд,
7
+1, я предпочитаю, чтобы в моем коде (т.е. __get__) не было вызовов магических функций . Я не знаю, для какой версии python вы это тестировали, но на python 3.4 MethodTypeфункция принимает два аргумента. Функция и экземпляр. Так что это следует изменить на types.MethodType(f, C()).
Дэн Милон
Вот! Это хороший способ исправить методы экземпляра:wgt.flush = types.MethodType(lambda self: None, wgt)
Winand
2
На самом деле это упоминается в документации, но на странице дескриптора из другого ответа: docs.python.org/3/howto/descriptor.html#functions-and-methods
kai
9

Создание замыкания с самим собой не будет технически связывать функцию, но это альтернативный способ решения той же (или очень похожей) основной проблемы. Вот банальный пример:

self.method = (lambda self: lambda args: self.do(args))(self)
Кейт Пинсон
источник
1
Да, это примерно то же самое, что и мое первоначальное исправление, которое нужно было использоватьfunctools.partial(handler, self)
Дэн Пассаро
7

Это будет привязано selfк handler:

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

Это работает путем передачи selfфункции в качестве первого аргумента. object.function()просто синтаксический сахар для function(object).

Брайен-бразилия
источник
1
Да, но это вызывает метод. Проблема в том, что мне нужно передать связанный метод как вызываемый объект. У меня есть несвязанный метод и экземпляр, к которому я бы хотел привязать его, но я не могу понять, как собрать все вместе, не вызывая его немедленно
Дэн Пассаро
9
Нет, он вызовет метод только в том случае, если вы выполните bound_handler (). Определение лямбда не вызывает лямбда.
brian-brazil,
1
Фактически вы можете использовать functools.partialвместо определения лямбда. Однако это не решает точную проблему. Вы по-прежнему имеете дело с a functionвместо instancemethod.
Алан Плам
5
@Alan: в чем разница между a, functionчей первый аргумент вы частично определили, и instancemethod; утка печатая не вижу разницы.
Ли Райан
2
@LieRyan, разница в том, что вы все еще не имеете дело с фундаментальным типом. functools.partialотбрасывает некоторые метаданные, например __module__. (Также я хочу заявить для записи, что я очень сильно съеживаюсь, когда смотрю на свой первый комментарий к этому ответу.) На самом деле, в моем вопросе я упоминаю, что уже использую, functools.partialно я чувствовал, что должен быть "более чистый" способ, поскольку легко получить как несвязанные, так и связанные методы.
Дэн Пассаро
1

Поздно к вечеринке, но я пришел сюда с аналогичным вопросом: у меня есть метод класса и экземпляр, и я хочу применить этот экземпляр к методу.

Рискуя чрезмерно упростить вопрос OP, я закончил тем, что сделал что-то менее загадочное, что может быть полезно для других, которые прибывают сюда (предостережение: я работаю в Python 3 - YMMV).

Рассмотрим этот простой класс:

class Foo(object):

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

    def value(self):
        return self._value

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

Вот что вы можете с этим сделать:

>>> 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
fearless_fool
источник
Это не решает мою проблему - я хотел, methчтобы меня можно было вызывать, не отправляя ему aаргумент (именно поэтому я изначально использовал functools.partial) - но это предпочтительнее, если вам не нужно передавать метод и можно просто вызвать его на месте. Также это работает в Python 2 так же, как и в Python 3.
Дэн Пассаро
Приносим извинения за то, что не прочитали ваши исходные требования более внимательно. Я неравнодушен (каламбур) к подходу на основе лямбда, представленному @ brian-brazil в stackoverflow.com/a/1015355/558639 - он настолько чист, насколько это возможно.
fearless_fool