def change_detection(cls):
    class NonExistentAttribute(object):
        pass

    class JNoneMeta(type):
        def __subclasscheck__(currentCls, parentCls):
            return currentCls == JNone and parentCls == type(None)

    class JBoolMeta(type):
        def __subclasscheck__(currentCls, parentCls):
            return currentCls == JBool and parentCls == type(bool)

    class JInt(int):
        pass

    class JString(str):
        pass

    class JBool(object, metaclass = JBoolMeta):
        def __init__(self, value):
            self._value = value

        def __bool__(self):
            return type(self._value) == type(bool) and self._value

        def __eq__(self, value):
            return self._value == value

    class JNone(object, metaclass = JNoneMeta):
        def __bool__(self):
            return False

        def __eq__(self, value):
            return value == None

    class Journaled(cls):
        @staticmethod
        def createAttribute(value, state):
            if value == None:
                value = JNone()
            elif isinstance(value, bool):
                value = JBool(value)
            elif isinstance(value, int):
                value = JInt(value)
            elif isinstance(value, str):
                value = JString(value)

            try: # for functions/methods but allows for lambda
                value.get_change = state
            except AttributeError:
                pass

            return value

        def __init__(self, *args, **kwargs):
            super().__setattr__("__modified__", set())
            super().__setattr__("__deleted__", set())
            super().__init__(*args, **kwargs)

        def __getattribute__(self, name):
            try:
                v = super().__getattribute__(name)
            except AttributeError:
                v = NonExistentAttribute()

            if not name.startswith("__"):
                if name in self.__deleted__:
                    s = "DEL"
                elif name in self.__modified__:
                    s = "MOD"
                else:
                    s = "INIT" if type(v) != NonExistentAttribute else ""
                return Journaled.createAttribute(v, s)

            return v

        def __setattr__(self, name, value):
            if not name.startswith("__") or name not in self.__modified__:
                try:
                    v = self.__getattribute__(name)
                    if type(v) != NonExistentAttribute and (v != value or typesAreDifferent(type(v), type(value))): 
                        self.__modified__.add(name)
                except AttributeError:
                    pass
            super().__setattr__(name, value)

        def __delattr__(self, name):
            if name in self.__modified__:
                self.__modified__.remove(name)
            if hasattr(self, name):
                self.__deleted__.add(name)
                super().__setattr__(name, None)

    def typesAreDifferent(subClass, parentClass):
        return not (issubclass(subClass, parentClass) or issubclass(parentClass, subClass))

    #copy original class attributes to Journaled class
    for clsAttr in filter(lambda x: not x.startswith("__"), dir(cls)):
        setattr(Journaled, clsAttr, cls.__dict__[clsAttr])

    return Journaled

@change_detection
class Struct:
    x = 42
    def __init__(self, y=0):
        self.y = y

a = Struct(11)

print("Struct.x ", Struct.x)
# Struct.x.get_change - will not be tested

a.x = 114
a.x.get_change == a.y.get_change == 'INIT'
Struct.x = 0
print(a.x)
a.z.get_change == ''

a.y = 11
a.y.get_change == 'INIT'

a.y = 12
a.y.get_change == 'MOD'

a.x = '42'
a.x.get_change == 'MOD'

del a.y
a.y.get_change == 'DEL'

Embed on website

To embed this program on your website, copy the following code and paste it into your website's HTML: