1

Is there some general way to get a class to run a function when any of its attributes are modified? I wondered if some subprocess could be running to monitor changes to the class, but maybe there's a way to inherit from class and modify some on_change function that is a part of the Python class, a bit like how the default __repr__ method of a class can be modified. What would be some sensible approach here?

The actual application is not to just do a printout but to update entries in a database that correspond to data attributes of instantiated classes.

#!/usr/bin/env python

class Event(object):
    def __init__(self):
        self.a = [10, 20, 30]
        self.b = 15
    #def _on_attribute_change(self):
    #    print(f'attribute \'{name_of_last_attribute_that_was_changed}\' changed')

event = Event()
event.a[1] = 25
# printout should happen here: attribute 'a' changed
event.a.append(35)
# printout should happen here: attribute 'a' changed
event.c = 'test'
# printout should happen here: attribute 'c' changed
BlandCorporation
  • 1,324
  • 1
  • 15
  • 33
  • Does this answer your question? [How to trigger function on value change?](https://stackoverflow.com/questions/6190468/how-to-trigger-function-on-value-change) – David Buck Dec 01 '19 at 17:24

3 Answers3

3

You can override the __setattr__ magic method.

class Foo:

    def on_change(self):
        print("changed")

    def __setattr__(self, name, value):
        self.__dict__[name] = value
        self.on_change()
Paul M.
  • 10,481
  • 2
  • 9
  • 15
  • Thanks muchly for your proposed solution there. I'm looking for something more general, like something that would cover data attributes experiencing an append operation or something, and the selected solution there appears to feature that functionality. – BlandCorporation Dec 04 '19 at 17:21
1

You can override __setattr__.

class Event:
    def __init__(self):
        self.a = [10, 20, 30]
        self.b = 15

    def __setattr__(self, attr, value):
        print(f'attribute {attr} changed')
        super().__setattr__(attr, value)

However, this only detects assignment directly to the attribute. event.a[1] = 25 is a call to event.a.__setitem__(1, 25), so Event knows nothing about it; it is handled entirely by whatever value event.a resolves to.

If you don't want the assignments in __init__ to trigger the notifications, call super().__setattr__ directly to avoid invoking your override.

def __init__(self):
    super().__setattr__('a', [10, 20, 30])
    super().__setattr(__('b', 15)
chepner
  • 497,756
  • 71
  • 530
  • 681
  • Thanks very much for your code there. It covers nearly all I need to do, but it doesn't cover the need to do things like append to a data attribute. I'm investigating the approach suggested by the selected solution. Thanks again for your solution. – BlandCorporation Dec 04 '19 at 17:22
1

Recently I developed server side on python, I had to detect changes on lists/dictionary/whatever you want, the library that saved my life is traits. I highly recommend it. you can easly check what changed/removed/added to your attribute.

you can read more here.

specifically for your case, the notification chapter is the most relevant

here's a small snippet I just ran:

from traits.api import *


class DataHandler(HasTraits):
    a = List([10, 20, 30])
    b = Int(15)


class Event(HasTraits):
    def __init__(self):
        super().__init__()
        self.data_handler = DataHandler()
        self.data_handler.on_trait_change(Event._anytrait_changed)

    @staticmethod
    def _anytrait_changed(obj, name, old, new):
        is_list = name.endswith('_items')
        if is_list:
            name = name[0:name.rindex('_items')]
        current_val = getattr(obj, name)
        if is_list:
            # new handles all the events(removed/changed/added to the list)
            if any(new.added):
                print("{} added to {} which is now {}".format(new.added, name, current_val))
            if any(new.removed):
                print("{} removed from {} which is now {}".format(new.removed, name, current_val))
        else:
            print('The {} trait changed from {} to {} '.format(name, old, (getattr(obj, name))))

e = Event()
e.data_handler.b = 13
e.data_handler.a.append(15)
e.data_handler.a.remove(15)
e.data_handler.a.remove(20)

outupts:

The b trait changed from 15 to 13 
[15] added to a which is now [10, 20, 30, 15]
[15] removed from a which is now [10, 20, 30]
[20] removed from a which is now [10, 30]

hope this helps.

nonamer92
  • 1,887
  • 1
  • 13
  • 24