Vlad Potra
Vlad Potra

Reputation: 309

Callback on variable change in python

I've searched around but didn't find anything like this. Let's say I have an army of threads, that keep reading and updating an integer variable x. I would like a callback for when x changes over a certain margin, let's say 500, to run the callback.

How can this be done without putting a heavy load on the system, like having a thread that has a while true and checks if the variable has changed? Performance is critical. But so are ethics.

In plain code would be something like this:

x = 10
def runMe():
    print('Its greater than 500!!') 

def whenToRun():
    return x >= 500

triggers.event(runMe, whenToRun)

Upvotes: 17

Views: 19355

Answers (2)

zvone
zvone

Reputation: 19382

You want to have a function (a "setter") which is called whenever the variable's value changes. A good way to do that is to define a @property. It will behave like a variable, but will have a getter function and a setter function.

Then, in the setter, call any callbacks you need, which will react to the change.

This should do the trick:

class ObjectHoldingTheValue:
    def __init__(self, initial_value=0):
        self._value = initial_value
        self._callbacks = []

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, new_value):
        old_value = self._value
        self._value = new_value
        self._notify_observers(old_value, new_value)

    def _notify_observers(self, old_value, new_value):
        for callback in self._callbacks:
            callback(old_value, new_value)

    def register_callback(self, callback):
        self._callbacks.append(callback)

Then, you can do:

def print_if_change_greater_than_500(old_value, new_value):
    if abs(old_value - new_value) > 500:
        print(f'The value changed by more than 500 (from {old_value} to {new_value})')

holder = ObjectHoldingTheValue()
holder.register_callback(print_if_change_greater_than_500)
holder.value = 7    # nothing is printed
holder.value = 70   # nothing is printed
holder.value = 700  # a message is printed

Upvotes: 33

abarnert
abarnert

Reputation: 366103

If x is an attribute of some object, rather than a global variable, this is very simple: add a __setattr__ method:

class MyType:
    def __setattr__(self, name, value):
        if name == 'x':
            self.x_callback(value)
        super().__setattr__(name, value)

Obviously there are ways you can make this more flexible:

  • Add a way to register conditions dynamically instead of just always calling x_callback.
  • Make it a mixin class that can be attached to any other class letting you register(name, callback) whatever you want.
  • Combine the above, so you can register(name, condition, callback).
  • self.x_callback(oldval=self.x, newval=value) so the callback can see old and new values.
  • if self.x_callback(value): so the callback can accept or reject the change.
  • value = self.x_callback(value) so the callback can override the change.

Upvotes: 7

Related Questions