Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Overriding __setattr__ at runtime

Tags:

python

I am trying to override the __setattr__ method of a Python class, since I want to call another function each time an instance attribute changes its value. However, I don't want this behaviour in the __init__ method, because during this initialization I set some attributes which are going to be used later:

So far I have this solution, without overriding __setattr__ at runtime:

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)

However, I would like to avoid these object.__setattr__(...) and override __setattr__ at the end of the __init__ method:

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)

I have tried with self.__dict__['__setitem__'] = self.aux and object.__setitem__['__setitem__'] = self.aux, but none of these attemps has effect. I have read this section of the data model reference, but it looks like the assignment of the own __setattr__ is a bit tricky.

How could be possible to override __setattr__ at the end of __init__, or at least have a pythonic solution where __setattr__ is called in the normal way only in the constructor?

like image 565
A. Rodas Avatar asked May 07 '13 18:05

A. Rodas


2 Answers

Unfortunately, there's no way to "override, after init" python special methods; as a side effect of how that lookup works. The crux of the problem is that python doesn't actually look at the instance; except to get its class; before it starts looking up the special method; so there's no way to get the object's state to affect which method is looked up.

If you don't like the special behavior in __init__, you could refactor your code to put the special knowledge in __setattr__ instead. Something like:

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)

Edit: Actually, there is a way to change which special method is looked up, so long as you change its class after it has been initialized. This approach will send you far into the weeds of metaclasses, so without further explanation, here's how that looks:

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
like image 174
SingleNegationElimination Avatar answered Sep 20 '22 12:09

SingleNegationElimination


@SingleNegationElimination's answer is great, but it cannot work with inheritence, since the child class's __mro__ store's the original class of super class. Inspired by his answer, with little change,

The idea is simple, switch __setattr__ before __init__, and restore it back after __init__ completed.

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
like image 22
Qiang Jin Avatar answered Sep 19 '22 12:09

Qiang Jin