Dynamically creating @attribute.setter methods for all properties in class (Python)

2.7k views Asked by At

I have code that someone else wrote like this:

class MyClass(object):
    def __init__(self, data):
        self.data = data

    @property
    def attribute1(self):
        return self.data.another_name1

    @property
    def attribute2(self):
        return self.data.another_name2

and I want to automatically create the corresponding property setters at run time so I don't have to modify the other person's code. The property setters should look like this:

    @attribute1.setter
    def attribue1(self, val):
        self.data.another_name1= val

    @attribute2.setter
    def attribue2(self, val):
        self.data.another_name2= val

How do I dynamically add these setter methods to the class?

2

There are 2 answers

1
Ashwini Chaudhary On

You can write a custom Descriptor like this:

from operator import attrgetter


class CustomProperty(object):
    def __init__(self, attr):
        self.attr = attr

    def __get__(self, ins, type):
        print 'inside __get__'
        if ins is None:
            return self
        else:
            return attrgetter(self.attr)(ins)

    def __set__(self, ins, value):
        print 'inside __set__'
        head, tail = self.attr.rsplit('.', 1)
        obj = attrgetter(head)(ins)
        setattr(obj, tail, value)


class MyClass(object):
    def __init__(self, data):
        self.data = data

    attribute1 = CustomProperty('data.another_name1')
    attribute2 = CustomProperty('data.another_name2')

Demo:

>>> class Foo():
...         pass
...
>>> bar = MyClass(Foo())
>>>
>>> bar.attribute1 = 10
inside __set__
>>> bar.attribute2 = 20
inside __set__
>>> bar.attribute1
inside __get__
10
>>> bar.attribute2
inside __get__
20
>>> bar.data.another_name1
10
>>> bar.data.another_name2
20
0
Jason Maldonis On

This is the author of the question. I found out a very jerry-rigged solution, but I don't know another way to do it. (I am using python 3.4 by the way.)

I'll start with the problems I ran into.

First, I thought about overwriting the property entirely, something like this:

Given this class

class A(object):
    def __init__(self):
        self._value = 42
    @property
    def value(self):
        return self._value

and you can over write the property entirely by doing something like this:

a = A()
A.value = 31 # This just redirects A.value from the @property to the int 31
a.value # Returns 31

The problem is that this is done at the class level and not at the instance level, so if I make a new instance of A then this happens:

a2 = A()
a.value # Returns 31, because the class itself was modified in the previous code block.

I want that to return a2._value because a2 is a totally new instance of A() and therefore shouldn't be influenced by what I did to a.

The solution to this was to overwrite A.value with a new property rather than whatever I wanted to assign the instance _value to. I learned that you can create a new property that instantiates itself from the old property using the special getter, setter, and deleter methods (see here). So I can overwrite A's value property and make a setter for it by doing this:

def make_setter(name):
    def value_setter(self, val):
        setattr(self, name, val)
    return value_setter
my_setter = make_setter('_value')
A.value = A.value.setter(my_setter) # This takes the property defined in the above class and overwrites the setter with my_setter
setattr(A, 'value', getattr(A, 'value').setter(my_setter)) # This does the same thing as the line above I think so you only need one of them

This is all well and good as long as the original class has something extremely simple in the original class's property definition (in this case it was just return self._value). However, as soon as you get more complicated, to something like return self.data._value like I have, things get nasty -- like @BrenBarn said in his comment on my post. I used the inspect.getsourcelines(A.value.fget) function to get the source code line that contains the return value and parsed that. If I failed to parse the string, I raised an exception. The result looks something like this:

def make_setter(name, attrname=None):
    def setter(self, val):
        try:
            split_name = name.split('.')
            child_attr = getattr(self, split_name[0])
            for i in range(len(split_name)-2):
                child_attr = getattr(child_attr, split_name[i+1])
            setattr(child_attr, split_name[-1], val)
        except:
            raise Exception("Failed to set property attribute {0}".format(name))

It seems to work but there are probably bugs.

Now the question is, what to do if the thing failed? That's up to you and sort of off track from this question. Personally, I did a bit of nasty stuff that involves creating a new class that inherits from A (let's call this class B). Then if the setter worked for A, it will work for the instance of B because A is a base class. However, if it didn't work (because the return value defined in A was something nasty), I ran a settattr(B, name, val) on the class B. This would normally change all other instances that were created from B (like in the 2nd code block in this post) but I dynamically create B using type('B', (A,), {}) and only use it once ever, so changing the class itself has no affect on anything else.

There is a lot of black-magic type stuff going on here I think, but it's pretty cool and quite versatile in the day or so I've been using it. None of this is copy-pastable code, but if you understand it then you can write your modifications.

I really hope/wish there is a better way, but I do not know of one. Maybe metaclasses or descriptors created from classes can do some nice magic for you, but I do not know enough about them yet to be sure.

Comments appreciated!