Search code examples
pythongetter-setter

Create a function that creates getters and setters for every attribute of an object to keep track of changes to object


I'm trying to keep track if any of the attributes of an object have been changed so I can keep a stack of object 'versions' that the user can revert to in case he needs (like a Undo\Redo feature).

For that, I figured I could use getters and setters for every one of the attributes, and have the setters call a function that keeps track of the changes. Since I have over 20 attributes, I'm trying to figure to do that without having to write 20 @property, @attribute.setter function pairs.

Something along the code below seems ideal. However, you can't use a variable as a function name and I can't figure out a good way to pass self to the inner attr and attr_setter functions.

class MyClass:
    def __init__(self, start, end):
        self._start = start
        self._end = end

        self.create_getset()

    def create_getset(self):
        for attr in self.__dict__:
            attr = attr[1:]
            @property
            def attr(self):
                print(f'{attr} getter is called')
                return self._attr

            @attr.setter
            def attr_setter(self, value):
                setattr(self, attr, value)
                # function to keep track of changes

Given that, some questions:

(1) Is this a good way to add getters and setters to all the attributes? Is there a better way?
(2) Is this a reasonably good way to keep track of the changes to the attributes?

If (1) and (2), then how can I make it work? How can I use attribute names as variables to create the functions needed to add getters and setters?


Solution

  • In the design you created here, there is a conflict: attribute names will only be known when the class is first instantiated, and attributes set - and, on the other hand, a property (or other descriptor) has to be set on the class - not on an instance, in order to work.

    A design similar to this one can work if you know all the attributes that will be set beforehand - at class build time. Then, you can simply call the create_getset from the class body itself, or, better yet, call it from an __init_subclass__ method in a base class: all you need is a way to keep track of what will be the versioned instance attributes (this can be done by declaring all attributes types, just as we do with dataclasses, or listing all atribute names in a class attribute.)

    class MyBase:
        
        def __init_subclass__(cls, *args, **kw):
            super().__init_subclass__(*args, **kw)
            
            def property_factory(attr):
                def getter(self):
                    print(f'{attr} getter is called')
                    return getattr(self, "_" + attr)
    
                def setter(self, value):
                    setattr(self, "_" + attr, value)
                    # function to keep track of changes
                    
                # The function-call semantics for creating a property will
                # be more convenient in this case
                return property(getter, setter)
            for attr in cls._attrs:
                setattr(cls, attr, property_factory(attr))
                
                    
    class MyClass(MyBase):
        _attrs = "start", "end"
        def __init__(self, start, end):
            self._start = start
            self._end = end
    
    

    For it to work with arbitrary dynamic attributes that are not known when the class code is written (say, an attribute that a user of the class will set on its instances), you can, instead, override the __setattr__ method - then you just won't need to declare properties at all:

    class MyBase:
        def __setattr__(self, attr, value):
            orig_setter = super().__setattr__
            if not hasattr(self, "_history"):
                orig_setter("_history", {})
            self._history.setdefault(attr, []).append(value)
            orig_setter(attr, value)
            
            
                    
    class MyClass(MyBase):
        def __init__(self, start, end):
            self.start = start
            self.end = end
    
    

    (This example includes a naive attribute-history keeping as well)