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?
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)