3

On the beginning, I know the bound methods attributes does not exist in Python 3 (according to this topic: Why does setattr fail on a bound method)

I'm trying to write a pseudo 'reactive' Python framework. Maybe I'm missing something and maybe, that what I'm trying to do is somehow doable. Lets look at the code:

from collections import defaultdict

class Event:
    def __init__(self):
        self.funcs = []

    def bind(self, func):
        self.funcs.append(func)

    def __call__(self, *args, **kwargs):
        for func in self.funcs:
            func(*args, **kwargs)


def bindable(func):
    events = defaultdict(Event)
    def wrapper(self, *args, **kwargs):
        func(self, *args, **kwargs)
        # I'm doing it this way, because we need event PER class instance
        events[self]()

    def bind(func):
        # Is it possible to somehow implement this method "in proper way"?
        # to capture "self" somehow - it has to be implemented in other way than now,
        # because now it is simple function not connected to an instance.
        print ('TODO')

    wrapper.bind = bind

    return wrapper

class X:
    # this method should be bindable - you should be able to attach callback to it
    @bindable
    def test(self):
        print('test')

# sample usage:

def f():
    print('calling f')

a = X()
b = X()

# binding callback
a.test.bind(f)

a.test() # should call f
b.test() # should NOT call f

Of course all classes, like Event were simplified for this example. Is there any way to fix this code to work? I want simply to be able to use bindable decorator to make a method (not a function!) bindable and be able to later "bind" it to a callback - in such way, that if somebody calls the method, the callback will be called automatically.

Is there any way in Python 3 to do it?

Community
  • 1
  • 1
Wojciech Danilo
  • 11,573
  • 17
  • 66
  • 132

2 Answers2

1

Ou yeah! :D I've found an answer - a little creazy, but working fast. If somebody has a comment or better solution, I would be very interested in seeing it. Following code is working for methods AND functions:

# ----- test classes -----    
class Event:
    def __init__(self):
        self.funcs = []

    def bind(self, func):
        self.funcs.append(func)

    def __call__(self, *args, **kwargs):
        message = type('EventMessage', (), kwargs)
        for func in self.funcs:
            func(message)

# ----- implementation -----

class BindFunction:
    def __init__(self, func):
        self.func = func
        self.event = Event()

    def __call__(self, *args, **kwargs):
        out = self.func(*args, **kwargs)
        self.event(source=None)
        return out

    def bind(self, func):
        self.event.bind(func)

class BindMethod(BindFunction):
    def __init__(self, instance, func):
        super().__init__(func)
        self.instance = instance

    def __call__(self, *args, **kwargs):
        out = self.func(self.instance, *args, **kwargs)
        self.event(source=self.instance)
        return out

class Descriptor(BindFunction):
    methods = {}

    def __get__(self, instance, owner):
        if not instance in Descriptor.methods:
            Descriptor.methods[instance] = BindMethod(instance, self.func)
        return Descriptor.methods[instance]

def bindable(func):
    return Descriptor(func)

# ----- usage -----
class list:
    def __init__(self, seq=()):
        self.__list = [el for el in seq]

    @bindable
    def append(self, p_object):
        self.__list.append(p_object)

    def __str__(self):
        return str(self.__list)

@bindable
def x():
    print('calling x')

# ----- tests -----

def f (event):
    print('calling f')
    print('source type: %s' % type(event.source))

def g (event):
    print('calling g')
    print('source type: %s' % type(event.source))

a = list()
b = list()

a.append.bind(f)
b.append.bind(g)

a.append(5)
print(a)

b.append(6)
print(b)

print('----')

x.bind(f)
x()

and the output:

calling f
source type: <class '__main__.list'>
[5]
calling g
source type: <class '__main__.list'>
[6]
----
calling x
calling f
source type: <class 'NoneType'>

The trick is to use Python's descriptors to store current instance pointer.

As a result we are able to bind a callback to any python function. The execution overhead is not too big - the empty function execution is 5 - 6 times slower than without this decorator. This overhead is caused by needed function chain and by event handling.

When using the "proper" event implementation (using weak references), like this one: Signal slot implementation, we are getting the overhead of 20 - 25 times the base function execution, which still is good.

EDIT: According to Hyperboreus question, I updated the code to be able to read from the callback methods the source object from whic the callbacks were called. They are now accessible by event.source variable.

Wojciech Danilo
  • 11,573
  • 17
  • 66
  • 132
  • What do you do when you need to access e.g. `b` from within `g`, i.e. the instance from within the "bound" method? – Hyperboreus Jan 31 '13 at 04:18
  • `g` is not a `bound` method in the Python meaning - maybe the name is a little confusing. `g` is callback to `b`, so whenever `b` is called, `g` will be also. This is intended to be usable while programming gui or whatever - for example, if a user adds something to a list, gui should change - we can 'subscribe' (maybe this is better word) to `list.append` with our callback method. After more thinking, what you propose could be usefull and I'm thinking about it right now. – Wojciech Danilo Jan 31 '13 at 04:27
  • Done. I redesigned the event flow, so now you can access the "the instance from within the bound method?" with `event.source` variable. Of course this is done in the simplest manner ever, but the idea is shown :) – Wojciech Danilo Jan 31 '13 at 04:42
  • The problem with this approach is that it keeps the instances alive as keys in Descript.methods. You should either use a WeakKeyDictionary, or store the binding in the instance's __dict__ (e.g. under '$bindings'). – Martin v. Löwis Jan 31 '13 at 12:56
  • You are right - In my 'final' implementation I'm using `WeakKeyDictionary`, but this answer was rather a "guidline". Events have exactly the same problem - they should be defined with `WeakKeyDictionary` for methods and `WeakSet`s for functions, but it will be no so straightforward to analyse this code :) – Wojciech Danilo Jan 31 '13 at 13:02
0

To be honest, I do not have an answer to your question, just another question is return:

Wouldn't monkey-patching your instances create the behaviour you intend:

#! /usr/bin/python3.2

import types

class X:
    def __init__ (self, name): self.name = name
    def test (self): print (self.name, 'test')

def f (self): print (self.name, '!!!')

a = X ('A')
b = X ('B')

b.test = types.MethodType (f, b) #"binding"

a.test ()
b.test ()
Hyperboreus
  • 31,997
  • 9
  • 47
  • 87
  • Yours answer is really interesting - I didn't know about `types.MethodType` as "manually method binding". Anyway, I don't see how coult it help with my problem :( – Wojciech Danilo Jan 31 '13 at 04:02
  • Sorry, then I misunderstood your question. Wasn't it about "binding" callables to a instance after instantiation and still receiving the `self` of the calling instance? – Hyperboreus Jan 31 '13 at 04:04
  • not exactly - I wanted to be able to BIND (in different meaning) any method / function to a callback in such way, that executing this method / function will execute the callback. This is a little bit complicated but **really** useful - I found the soulution - please see my answer. But if you know a soution, which will be more beautifull or simply other than mine, I would love to see it :) – Wojciech Danilo Jan 31 '13 at 04:07