Переопределение __setattr__ во время выполнения

Я пытаюсь переопределить метод __setattr__ класса Python, так как я хочу вызывать другую функцию каждый раз, когда атрибут экземпляра меняет свое значение. Однако я не хочу этого поведения в методе __init__, потому что во время этой инициализации я устанавливаю некоторые атрибуты, которые будут использоваться позже:

Пока у меня есть это решение без переопределения __setattr__ во время выполнения:

class Foo(object):
    def __init__(self, a, host):
        object.__setattr__(self, 'a', a)
        object.__setattr__(self, 'b', b)
        result = self.process(a)
        for key, value in result.items():
            object.__setattr__(self, key, value)

    def __setattr__(self, name, value):
        print(self.b) # Call to a function using self.b
        object.__setattr__(self, name, value)

Однако я хотел бы избежать этих object.__setattr__(...) и переопределить __setattr__ в конце метода __init__:

class Foo(object):
    def __init__(self, a, b):
        self.a = a
        self.b = b
        result = self.process(a)
        for key, value in result.items():
            setattr(self, key, value)
        # override self.__setattr__ here

    def aux(self, name, value):
        print(self.b)
        object.__setattr__(self, name, value)

Я пробовал с self.__dict__['__setitem__'] = self.aux и object.__setitem__['__setitem__'] = self.aux, но ни один из этих attemps не имеет эффекта. Я прочитал этот раздел ссылки на модель данных, но похоже, что назначение собственного __setattr__ немного сложно.

Как можно переопределить __setattr__ в конце __init__ или, по крайней мере, иметь питоновское решение, где __setattr__ вызывается обычным способом только в конструкторе?

Ответ 1

К сожалению, нет способа "переопределить, после init" специальные методы python; как побочный эффект того, как работает этот поиск. Суть проблемы в том, что python фактически не смотрит на экземпляр; кроме того, чтобы получить свой класс; прежде чем он начнет искать специальный метод; поэтому нет способа заставить состояние объекта влиять на то, какой метод просматривается.

Если вам не нравится специальное поведение в __init__, вы можете реорганизовать свой код, чтобы вместо этого добавить специальные знания в __setattr__. Что-то вроде:

class Foo(object):
    __initialized = False
    def __init__(self, a, b):
        try:
            self.a = a
            self.b = b
            # ...
        finally:
            self.__initialized = True

    def __setattr__(self, attr, value):
        if self.__initialzed:
            print(self.b)
        super(Foo, self).__setattr__(attr, value)

Изменить: На самом деле есть способ изменить, какой специальный метод просматривается, если вы измените его класс после его инициализации. Этот подход пошлет вас далеко в сорняки метаклассов, поэтому без дальнейших объяснений, вот как это выглядит:

class AssignableSetattr(type):
    def __new__(mcls, name, bases, attrs):
        def __setattr__(self, attr, value):
            object.__setattr__(self, attr, value)

        init_attrs = dict(attrs)
        init_attrs['__setattr__'] = __setattr__

        init_cls = super(AssignableSetattr, mcls).__new__(mcls, name, bases, init_attrs)

        real_cls = super(AssignableSetattr, mcls).__new__(mcls, name, (init_cls,), attrs)
        init_cls.__real_cls = real_cls

        return init_cls

    def __call__(cls, *args, **kwargs):
        self = super(AssignableSetattr, cls).__call__(*args, **kwargs)
        print "Created", self
        real_cls = cls.__real_cls
        self.__class__ = real_cls
        return self


class Foo(object):
    __metaclass__ = AssignableSetattr

    def __init__(self, a, b):
        self.a = a
        self.b = b
        for key, value in process(a).items():
            setattr(self, key, value)

    def __setattr__(self, attr, value):
        frob(self.b)
        super(Foo, self).__setattr__(attr, value)


def process(a):
    print "processing"
    return {'c': 3 * a}


def frob(x):
    print "frobbing", x


myfoo = Foo(1, 2)
myfoo.d = myfoo.c + 1

Ответ 2

@SingleNegationElimination ответ велик, но он не может работать с наследованием, так как дочерний класс __mro__ хранит исходный класс суперкласса. Вдохновленный его ответом, с небольшими изменениями,

Идея проста, переключитесь __setattr__ до __init__ и верните ее после завершения __init__.

class CleanSetAttrMeta(type):
    def __call__(cls, *args, **kwargs):
        real_setattr = cls.__setattr__
        cls.__setattr__ = object.__setattr__
        self = super(CleanSetAttrMeta, cls).__call__(*args, **kwargs)
        cls.__setattr__ = real_setattr
        return self


class Foo(object):
    __metaclass__ = CleanSetAttrMeta

    def __init__(self):
        super(Foo, self).__init__()
        self.a = 1
        self.b = 2

    def __setattr__(self, key, value):
        print 'after __init__', self.b
        super(Foo, self).__setattr__(key, value)


class Bar(Foo):
    def __init__(self):
        super(Bar, self).__init__()
        self.c = 3

>>> f = Foo()
>>> f.a = 10
after __init__ 2
>>>
>>> b = Bar()
>>> b.c = 30
after __init__ 2