Есть ли способ сообщить (перехватить) модификацию атрибута класса? - PullRequest
0 голосов
/ 18 апреля 2020

Впервые на Python, пытаясь решить проблему, в которой мне нужно реализовать декоратор класса, который будет отслеживать изменения его атрибутов класса и экземпляра. Декоратору необходимо добавить атрибут get_change ко всем атрибутам класса и экземпляра, чтобы отслеживать их состояние (INIT, MODIFIED, DELETED), соответствующее начальному значению, измененному значению и удаленному атрибуту. По большей части я решил это за исключением одного крайнего случая: изменение, удаление атрибута класса.

@change_detection
class Struct(object):
    x = 42

    def __init__(self, y=0):
        self.y = y

a = Struct(11)

a.x == Struct.x == 42 # True
a.y == 11 # True

a.x.get_change == Struct.x.get_change == "INIT" # True
a.y.get_change == "INIT" # True

a.x = 100
a.x.get_change == "MOD" # True

del a.x
a.x.get_change == "DEL" # True

Я застрял с атрибутом класса как изменения:

Struct.x = 10
Struct.x.get_change == "MOD" # False - I don't know how to intercept setting the class attribute
del Struct.x
Struct.x.get_change == "DEL" # False - same as before

Итак, как вы перехватываете установку классов, удаление атрибутов? Для уровня экземпляра у вас есть __setattr__ и __delattr__, но каков эквивалент уровня класса, если таковой имеется?

Спасибо!

Ответы [ 2 ]

1 голос
/ 21 апреля 2020

Благодаря указателям от @Felipe удалось решить все вопросы. Опять же, это не практическая проблема, скорее, проблема кода , описанная здесь .

Идея состояла в том, чтобы динамически создать подкласс для декорированного класса и вернуть прокси-объекты, содержащие атрибут get_change, в дополнение к атрибуты объекта прокси.

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
1 голос
/ 18 апреля 2020

Не существует метода волхвов c (насколько мне известно - возможно, здесь есть какой-то хак), который работает с атрибутами класса. Вместо этого вы можете сделать что-то вроде этого:

class Verifier:
    def __init__(self, obj):
        self.obj = obj
        self.init = obj.__dict__.copy()

    def get_change(self, var):
        if var not in self.obj.__dict__:
            return "DEL"
        elif self.obj.__dict__[var] == self.init[var]:
            return "INIT"
        elif self.obj.__dict__[var] != self.init[var]:
            return "MOD"

class Struct:
    x = 42

verifier = Verifier(Struct)

Это позволит следующее:

Struct.x = 42
print(verifier.get_change("x")) # INIT

Struct.x = 43
print(verifier.get_change("x")) # MOD

del Struct.x
print(verifier.get_change("x")) # DEL

Однако обратите внимание, что это сломается:

Struct.y = 40
print(verifier.get_change("y"))
Traceback (most recent call last):
  File "test.py", line 26, in <module>
    print(verifier.get_change("y"))
  File "test.py", line 9, in get_change
    elif self.obj.__dict__[var] == self.init[var]:
KeyError: 'y'

Поскольку наш Verifier имеет доступ только к более старому Struct, у которого не было переменной y.


Редактировать (3.0): Текущий прогресс. Решил добавить его сюда на тот случай, если вы захотите проверить, что у меня сейчас есть, так как это может помочь вам решить вашу собственную проблему:

def Proxy(val):
    try:
        class Obj(type(val)): pass
    except:
        class Obj(): pass

    class Proxy(Obj):
        def __init__(self, val):
            self.val = val
            self.old = val

            self.modified = False
            self.deleted = False

        @property
        def get_change(self):
            if type(self.val) == type(NONE):
                return ""
            elif self.deleted:
                return "DEL"
            elif self.val is not self.old or self.modified or self.val != self.old:
                return "MOD"
            elif self.val is self.old  or self.val == self.old:
                return "INIT"

        def __getattr__(self, attr):
            return getattr(self.val, attr)

        def __repr__(self):
            return repr(self.val)

        def __eq__(self, val):
            if self.val == val:
                return True
            else:
                return super(Proxy, self).__eq__(val)

        def __bool__(self):
            if self.val == None:
                return False
            else:
                return not self.val

    return Proxy(val)


def change_detection(cls):

    class cls_new(cls):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)

        def __getattribute__(self, attr):
            return super(cls_new, self).__getattribute__(attr)

        def __getattr__(self, attr):
            return Proxy(NONE)

        def __setattr__(self, attr, val):
            if not attr.startswith("__"):
                value = Proxy(val)

                # Checks if attr in instance dictionary.
                if attr in self.__class__.__dict__:
                    value.old = self.__class__.__dict__[attr].old
                elif attr in self.__dict__:
                    value.old = self.__dict__[attr].old

                    if self.__dict__[attr] != val and val is None:
                        value.modified = True

            else:
                value = val

            super(self.__class__, self).__setattr__(attr, value)

        def __delattr__(self, attr):
            if attr in self.__class__.__dict__:
                self.__class__.__dict__[attr].val = None
                self.__class__.__dict__[attr].deleted = True

            if attr in self.__dict__:
                self.__dict__[attr].val = None
                self.__dict__[attr].deleted = True



    try:
        # Copies class attributes to cls_new.__class__.__dict__ as Proxy objects.
        for attr in dir(cls()):
            if not callable(getattr(cls(), attr)) and not attr.startswith("__") and attr in cls.__dict__:
                setattr(cls_new, attr, Proxy(cls.__dict__[attr]))

        for attr in dir(cls):
            if not attr.startswith("__") and callable(cls.__dict__[attr]) and cls.__dict__[attr].__name__ == (lambda: 0).__name__:
                setattr(cls_new, attr, Proxy(cls.__dict__[attr]))
    except:
        pass

    return cls_new
...