I would like to create a data structure containing several settings, these settings will be used to calculate register values of a hardware device. To avoid reconfiguring all settings of the hardware device, I would like to have each variable inside of the data structure remember if it has been changed or not. Then later I would call upon all variables to see which ones are changed to then only write to the connected registers.
I can create a class that remembers if any change has occurred to it's internally stored value, I am however experiencing difficulties with returning and resetting the has_changed
variable. This due to the overloading of the __get__
function prohibiting the usage of other functions inside of the class.
In the simplified example I have made a class called Table
(which should contain variables such as: height
, width
, length
, ...) The current implementation has the class TrackedValidatedInteger
which checks if the change is valid.
I would like the variable property has_changed
to be obtainable and resettable from inside of the class Table
.
class TrackedValidatedInteger():
def __init__(self, min_value=None, max_value=None):
self.min_value = min_value
self.max_value = max_value
self.has_changed = False
self.value = None
def __get__(self, obj, objecttype=None):
return self.value
def __set__(self, obj, value):
if self.validate_set(value):
self.value = value
self.has_changed = True
return 1
return 0
def get_has_changed(self):
return self.has_changed
def reset_has_changed(self):
self.has_changed = False
def validate_set(self, value):
if self.min_value:
if self.min_value > value:
print("Value should be between " + str(self.min_value) + " and " + str(self.max_value))
return 0
if self.max_value:
if self.max_value < value:
print("Value should be between " + str(self.min_value) + " and " + str(self.max_value))
return 0
return 1
class Table():
length = TrackedValidatedInteger(min_value=0, max_value=3)
height = TrackedValidatedInteger(min_value=0, max_value=6)
width = TrackedValidatedInteger(min_value=0, max_value=7)
def __init__(self, length=0, height=0, width=0):
self.length = length
self.height = height
self.width = width
def reset_has_changed_1(self):
self.length.has_changed = False
self.height.has_changed = False
self.width.has_changed = False
def reset_has_changed_2(self):
self.length.reset_has_changed()
self.height.reset_has_changed()
self.width.reset_has_changed()
p = Table()
p.length = 3 # will set the variable
p.length = 9 # will not set the variable
# p.length.get_has_changed() # This does not work as the p.length will call __get__ resulting in an integer which does not have get_has_changed()
# p.reset_has_changed_1() # This does not work for the same reason
# p.reset_has_changed_2() # This does not work for the same reason
The problem I find is that the __get__
function gets automatically called whenever I try to access any other part of the TrackedValidatedInteger
class. Can I access the other variables and functions in any other way? If there are any suggestions on how achieve the same result in another way, I would be glad to hear it. I would personally like to keep the simple setting of the variables (p.length = 3
), if not possible this can be changed.
Any help would be greatly appreciated.
I like the idea of doing this from a descriptor. You can take advantage of the fact that a descriptor can know the name of the attribute to which it is bound via the __set_name__
method, and use that to maintain attributes on the target object:
class TrackedValidatedInteger:
def __init__(self, min_value=None, max_value=None):
self.min_value = min_value
self.max_value = max_value
self.has_changed = False
self.value = None
def __set_name__(self, obj, name):
self.name = name
setattr(obj, f"{self.name}_changed", False)
def __get__(self, obj, objecttype=None):
return self.value
def __set__(self, obj, value):
if (self.min_value is not None and value < self.min_value) or (
self.max_value is not None and value > self.max_value
):
raise ValueError(
f"{value} must be >= {self.min_value} and <= {self.max_value}"
)
self.value = value
setattr(obj, f"{self.name}_changed", True)
Given the above implementation, we can create a class Example
like this:
class Example:
v1 = TrackedValidatedInteger()
v2 = TrackedValidatedInteger()
And then observe the following behavior:
>>> e = Example()
>>> e.v1_changed
False
>>> e.v1 = 42
>>> e.v1_changed
True
>>> e.v2_changed
False
>>> e.v2 = 0
>>> e.v2_changed
True
Instead of maintaining a per-attribute <name>_changed
variable, you could instead maintain a set
of changed attributes:
class TrackedValidatedInteger:
def __init__(self, min_value=None, max_value=None):
self.min_value = min_value
self.max_value = max_value
self.has_changed = False
self.value = None
def __set_name__(self, obj, name):
self.name = name
if not hasattr(obj, "_changed_attributes"):
setattr(obj, "_changed_attributes", set())
def __get__(self, obj, objecttype=None):
return self.value
def __set__(self, obj, value):
if (self.min_value is not None and value < self.min_value) or (
self.max_value is not None and value > self.max_value
):
raise ValueError(
f"{value} must be >= {self.min_value} and <= {self.max_value}"
)
self.value = value
obj._changed_attributes.add(self.name)
In that case, we get:
>>> e = Example()
>>> e._changed_attributes
set()
>>> e.v1 = 1
>>> e._changed_attributes
{'v1'}
>>> e.v2 = 1
>>> e._changed_attributes
{'v1', 'v2'}
This is nice because you can iterate over e._changed_attributes
if you need to record all your changed values.