Search code examples
pythoncallbackmagic-methods

Python Model Callbacks


I'm trying to implement a simple model class that executes callbacks whenever an attribute is set in the class, however I am getting an AttributeError when trying to use this in my application. It seems the instance doesn't have the attributes I setup in my initializer and I'm a bit confused as to why. I haven't used magic methods much, so some elaboration might be helpful:

class ReportModel(object):

    def __init__(self):
        self.current_date = None
        self.prior_date = None
        self._callbacks = defaultdict([])

    def __setattr__(self, attr, value):
        object.__setattr__(self, attr, value)
        for func in self._callbacks[attr]:
            func(value)

    def set_callback(self, attr, function):
        self._callbacks[attr].append(function)

Traceback:

AttributeError: 'ReportModel' object has no attribute '_callbacks'

Solution

  • Since you have overridden __setattr__(), when you do self.current_date = None in __init__() it will call your __setattr__() implementation which will try to access self._callbacks. This is what causes the AttributeError.

    Here is one option to fix this:

    class ReportModel(object):
    
         def __init__(self):
             object.__setattr__(self, '_callbacks', defaultdict(list))
             self.current_date = None
             self.prior_date = None
    
         def __setattr__(self, attr, value):
             object.__setattr__(self, attr, value)
             for func in self._callbacks[attr]:
                 func(value)
    
         def set_callback(self, attr, function):
             self._callbacks[attr].append(function)
    

    This adds the _callbacks attribute using object.__setattr__() before any other attributes are added, so that future calls to __setattr__() for your ReportModel instance will not choke on accessing self._callbacks.

    Alternatively, you could modify __setattr__() to check for the existence of the _callbacks attribute:

    class ReportModel(object):
    
         def __init__(self):
             object.__setattr__(self, '_callbacks', defaultdict(list))
             self.current_date = None
             self.prior_date = None
    
         def __setattr__(self, attr, value):
             object.__setattr__(self, attr, value)
             if hasattr(self, '_callbacks'):
                 for func in self._callbacks[attr]:
                     func(value)
    
         def set_callback(self, attr, function):
             self._callbacks[attr].append(function)