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
Well, using @Diego´s answer (which I will be accepting) and some workarounds to achieve my personal needs I managed to this:
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
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.
I thing there is an easiest way to achive the behavior : overloading __getattr__
and __setattr__
functions.
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.
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 !