Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python 3 bound methods subscription

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?

like image 471
Wojciech Danilo Avatar asked Aug 29 '26 08:08

Wojciech Danilo


1 Answers

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.

like image 197
Wojciech Danilo Avatar answered Aug 31 '26 20:08

Wojciech Danilo



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!