Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Why is my Python decorator class's __get__ method not called in all cases?

So I’m trying to implement something akin to C# events in Python as a decorator for methods:

from __future__ import annotations
from typing import *
import functools


def event(function: Callable) -> EventDispatcher:
    return EventDispatcher(function)


class EventDispatcher:

    def __init__(self, function: Callable) -> None:
        functools.update_wrapper(self, function)

        self._instance: Any | None = None
        self._function: Callable = function
        self._callbacks: Set[Callable] = set()

    def __get__(self, instance: Any, _: Any) -> EventDispatcher:
        self._instance = instance
        return self

    def __iadd__(self, callback: Callable) -> EventDispatcher:
        self._callbacks.add(callback)
        return self

    def __isub__(self, callback: Callable) -> EventDispatcher:
        self._callbacks.remove(callback)
        return self

    def __call__(self, *args: Any, **kwargs: Any) -> Any:
        for callback in self._callbacks:
            callback(*args, **kwargs)

        return self._function(self._instance, *args, **kwargs)

But when I decorate a class method with @event and later on call the decorated method, the method will be invoked on the incorrect instance in some cases.

Good Case:

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

class A:

    @event
    def my_event(self) -> None:
        print(self)


class B:

    def __init__(self, a: A) -> None:
        a.my_event += self.my_callback

    def my_callback(self) -> None:
        pass


a0 = A()
a1 = A()

# b = B(a0)

a1.my_event()
a0.my_event()

The above code will result in the output:

<__main__.A object at 0x00000170AA15FCA0>
<__main__.A object at 0x00000170AA15FC70>

Evidently the function my_event() is called twice, each time with a different instance as expected.

Bad Case:

Taking the code from the good case and commenting in the line # b = B(a0) results in the output:

<__main__.A object at 0x000002067650FCA0>
<__main__.A object at 0x000002067650FCA0>

Now the method my_event() is called twice, too. But on the same instance.

Question:

I think the issue boils down to EventDispatcher.__get__() not being called in the bad case. So my question is, why is EventDispatcher.__get__() not called and how do I fix my implementation?

>Solution :

The problem lies in the initializer of B:

a.my_event += self.my_callback

Note that the += operator is not just a simple call to __iadd__, but actually equivalent to:

a.my_event = a.my_event.__iadd__(self.my_callback)

This is also the reason why your __iadd__ method needs to return self.

Because the class EventDispatcher has only __get__ but no __set__, the result will be written to the instance’s attribute during assignment, so the above statement is equivalent to:

a.__dict__['my_event'] = A.__dict__['my_event'].__get__(a, A).__iadd__(self.my_callback)

Simple detection:

print(a0.__dict__)
b = B(a0)
print(a0.__dict__)

Output:

{}
{'my_event': <__main__.EventDispatcher object at 0x00000195218B3FD0>}

When a0 calls my_event on the last line, it only takes the instance of EventDispatcher from a0.__dict__, and does not trigger the __get__ method. Therefore, A.__dict__['my_event']._instance will not be updated.

The simplest repair way is to add an empty __set__ method to the definition of EventDispatcher:

class EventDispatcher:
    ...

    def __set__(self, instance, value):
        pass

    ...

Output:

<__main__.A object at 0x000002A36B9B3D30>
<__main__.A object at 0x000002A36B9B3D00>
Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading