Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Tracking change of variable in Python

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.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

Any help would be greatly appreciated.

>Solution :

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.

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading