How do I call a specific function when a class variable changes?

43 Views Asked by At

I've created a class and initialized three variables a, b and c. Now I want to call a specific function func1 whenever the variables a or c are changed from the outside, and a function func2 when variable b is changed from the outside.

I am aware that this can be done using decorators, like this:

class Event:
    def __init__(self, a, b, c):
        self._a = a
        self._b = b
        self._c = c

    @property
    def a(self):
        return self._a
    @a.setter
    def a(self, value):
        self._a = value
        print("Variable a changed!")
        self.func1()

    @property
    def b(self):
        return self._b
    @b.setter
    def b(self, value):
        self._b = value
        print("Variable b changed!")
        self.func2()

    @property
    def c(self):
        return self._c
    @c.setter
    def c(self, value):
        self._c = value
        print("Variable c changed!")
        self.func1()

    def func1(self):
        print("Function 1 called")
    def func2(self):
        print("Function 2 called")

obj = Event(1, 2, 3)
obj.a = 15
obj.b = 10
obj.c = 5

My final code will have 8 or more variables however, and writing a designated @property and @var.setter for every single one of them will be very cumbersome and not really readable.

Is there a simpler way to just say If variables a, c, f, ... are updated, call function X, if b, e, ... are updated, call function Y?

Thank you!

2

There are 2 best solutions below

3
cards On BEST ANSWER

You could subclass property with custom features. Here a basic example on how to do that. Notice you should provide a dictionary which maps which property triggers which function (as string).

class TriggerProperty(property):

    MAPPER = {'a': 'func1', 'b': 'func2', 'c': 'func1'}
    
    def __set__(self, obj, value):
        super().__set__(obj, value)
        func_name = self.MAPPER.get(self.fget.__name__)
        getattr(obj, func_name)()


class Event:
    # ...
    @TriggerProperty
    def a(self):
        return self._a

    @a.setter
    def a(self, value):
        self._a = value
        print("Variable a changed!")

    # ...
7
chepner On

You can define a single property-like descriptor and use that to define each variable.

class TriggerProperty:
    def __init__(self, callback):
        self.callback = callback

    def __set_name__(self, obj, name):
        self.private_name = "_" + name
        self.public_name = name

    def __get__(self, obj, obj_type):
        return getattr(obj, self.private_name)

    def __set__(self, obj, v):
        # Don't do this if the attribute is being created
        if hasattr(obj, self.private_name):
            self.callback()
            print(f'{self.public_name} changed!')

        return setattr(obj, self.private_name, v)

def func1():
    print("Function 1 called")

def func2():
    print("Function 2 called")

class Event:
    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c

    a = TriggerProperty(func1)
    b = TriggerProperty(func2)
    c = TriggerProperty(func1)

Then

>>> e = Event(1,2,3)
>>> e.a = 5
Function 1 called
a changed!
>>> e.b = 7
Function 2 called
b changed!
>>> e.c = 9
Function 1 called
c changed!

The descriptor how-to provides an example of how property might be defined in pure Python. You might want to study that to see if there is a way to define TriggerProperty as a subclass of property, instead of implementing the descriptor protocol explicitly as done above.