Search code examples
pythonpython-3.xpython-decoratorsmagic-methodspython-descriptors

How to decorate a class and use descriptors to access properties?


I am trying to master (begin ;)) to understand how to properly work with decorators and descriptors on Python 3. I came up with an idea that i´m trying to figure how to code it.

I want to be able to create a class A decorated with certain "function" B or "class" B that allows me to create a instance of A, after delaring properties on A to be a component of certain type and assigning values on A __init__ magic function. For instance:

componentized is certain "function B" or "class B" that allows me to declarate a class Vector. I declare x and y to be a component(float) like this:

@componentized 
class Vector: 
    x = component(float)
    y = component(float) 
    def __init__ (self, x, y): 
        self.x = x 
        self.y = y

What I have in mind is to be able to this:

v = Vector(1,2)
v.x #returns 1

But the main goal is that I want do this for every marked component(float) property:

v.xy #returns a tuple (1,2)
v.xy = (3,4) #assigns to x the value 3 and y the value 4

My idea is to create a decorator @componentized that overrides the __getattr__ and __setattr__ magic methods. Sort of this:

def componentized(cls):
    class Wrapper(object):
        def __init__(self, *args):
            self.wrapped = cls(*args)

        def __getattr__(self, name):
            print("Getting :", name)

            if(len(name) == 1):
                return getattr(self.wrapped, name)

            t = []
            for x in name:
                t.append(getattr(self.wrapped, x))

            return tuple(t)

@componentized
class Vector(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y

And it kind of worked, but i don't think I quite understood what happened. Cause when I tried to do an assign and override the __setattr__ magic method it gets invoked even when I am instantiating the class. Two times in the following example:

vector = Vector(1,2)
vector.x = 1

How would could I achieve that sort of behavior?

Thanks in advance! If more info is needed don't hesitate to ask!

EDIT:

Following @Diego's answer I manage to do this:

def componentized(cls):
    class wrappedClass(object):
        def __init__(self, *args, **kwargs):
            t = cls(*args,**kwargs)
            self.wrappedInstance = t

        def __getattr__(self, item):
            if(len(item) == 1):
                return self.wrappedInstance.__getattribute__(item)
            else:
                return tuple(self.wrappedInstance.__getattribute__(char) for char in item)

        def __setattr__(self, attributeName, value):
            if isinstance(value, tuple):
                for char, val in zip(attributeName, value):
                    self.wrappedInstance.__setattr__(char, val)
            elif isinstance(value, int): #EMPHASIS HERE
                for char in attributeName:
                    self.wrappedInstance.__setattr__(char, value)
            else:
                object.__setattr__(self, attributeName, value)
    return wrappedClass

And Having a class Vector like this:

@componentized 
class Vector:
    def __init__ (self, x, y): 
        self.x = x 
        self.y = y

It kind of behave like I wanted, but I still have no idea how to achieve:

x = component(float)
y = component(float)

inside the Vector class to somehow subscribe x and y of type float, so when I do the #EMPHASIS LINE(in the line I hardcoded a specific type) on the code I can check whether the value someone is assigning a value to x and/or y for an instance of Vector is of type I defined it with:

x = component(float)

So I tried this (a component (descriptor) class):

class component(object):
    def __init__(self, t, initval=None):
        self.val = initval
        self.type = t

    def __get__(self, obj, objtype):
        return self.val

    def __set__(self, obj, val):
        self.val = val

To use component like a descriptor, but I couldn't managed to do a workaround to handle the type. I tried to do an array to hold val and type, but then didn't know how to get it from the decorator __setattr__ method.

Can you point me into the right direction?

PS: Hope you guys understand what I am trying to do and lend me a hand with it. Thanks in advance

Solution

Well, using @Diego´s answer (which I will be accepting) and some workarounds to achieve my personal needs I managed to this:

Decorator (componentized)

def componentized(cls):
    class wrappedClass(object):
        def __init__(self, *args):
            self.wrappedInstance = cls(*args)

        def __getattr__(self, name):
             #Checking if we only request for a single char named value
             #and return the value using getattr() for the wrappedInstance instance
             #If not, then we return a tuple getting every wrappedInstance attribute
            if(len(name) == 1):
                return getattr(self.wrappedInstance, name)
            else:
                return tuple(getattr(self.wrappedInstance, char) for char in name)  

        def __setattr__(self, attributeName, value):
            try:
                #We check if there is not an instance created on the wrappedClass __dict__
                #Meaning we are initializing the class
                if len(self.__dict__) == 0:
                    self.__dict__[attributeName] = value
                elif isinstance(value, tuple): # We get a Tuple assign
                    self.__checkMultipleAssign(attributeName)
                    for char, val in zip(attributeName, value):
                        setattr(self.wrappedInstance, char, val)
                else:
                    #We get a value assign to every component
                    self.__checkMultipleAssign(attributeName)
                    for char in attributeName:
                        setattr(self.wrappedInstance, char, value)
            except Exception as e:
                print(e)

        def __checkMultipleAssign(self, attributeName):
            #With this we avoid assigning multiple values to the same property like this
            # instance.xx = (2,3) => Exception
            for i in range(0,len(attributeName)):
                for j in range(i+1,len(attributeName)):
                    if attributeName[i] == attributeName[j]:
                        raise Exception("Multiple component assignment not allowed")
    return wrappedClass

component (descriptor class)

class component(object):
    def __init__(self, t):
        self.type = t #We store the type
        self.value = None #We set an initial value to None

    def __get__(self, obj, objtype):
        return self.value #Return the value

    def __set__(self, obj, value):
        try:
            #We check whether the type of the component is diferent to the assigned value type and raise an exeption
            if self.type != type(value):
                raise Exception("Type \"{}\" do not match \"{}\".\n\t--Assignation never happened".format(type(value), self.type))
        except Exception as e:
            print(e)
        else:
            #If the type match we set the value
            self.value = value

(The code comments are self explanatories)

With this design I can achieve what I wanted (explained above) Thanks you all for your help.


Solution

  • I thing there is an easiest way to achive the behavior : overloading __getattr__and __setattr__ functions.

    Getting vector.xy :

    class Vector:
        ...
    
        def __getattr__(self, item):
             return tuple(object.__getattribute__(self, char) for char in item)
    

    The __getattr__ function is called only when "normal" ways of accessing an atribute fails, as stated in the Python documentation. So, when python doesn't find vector.xy, the __getattr__method is called and we return a tuple of every value (ie. x and y).
    We use object.__getattribute__ to avoid infinite recurtion.

    Setting vector.abc :

        def __setattr__(self, key, value):
    
            if isinstance(value, tuple) and len(key) == len(value):
                for char, val in zip(key, value):
                    object.__setattr__(self, char, val)
    
            else:
                object.__setattr__(self, key, value)
    

    The __setattr__ method is always called unlike __getattr__, so we set each value separately only when the item we want to set is of the same lenght as the tuple of value.

    >>> vector = Vector(4, 2)
    >>> vector.x
    4
    >>> vector.xy 
    (4, 2)
    >>> vector.xyz = 1, 2, 3
    >>> vector.xyxyxyzzz
    (1, 2, 1, 2, 1, 2, 3, 3, 3)
    

    The only drawback is that if you really want to asign a tuple like (suppose you have an attribute called size):

    vector.size = (1, 2, 3, 4)
    

    Then s, i, z and e will by assigned separately, and that's obviously not what you want !