Classes [advanced]: Dependent attributes (and Descriptors)

A place where you can post Python-related tutorials you made yourself, or links to tutorials made by others.

Classes [advanced]: Dependent attributes (and Descriptors)

Postby Mekire » Fri Jul 26, 2013 1:56 am

Sometimes in our classes it becomes convenient, and occasionally necessary, to have attributes which are dependent on one another. Before beginning I would like to note that there are many different ways to accomplish this; I will try to cover the most useful and easiest to implement.

Let's consider a simple class representing a rectangle. Our class will have four attributes; x, y, width, and height.
Code: Select all
class Rect(object):
    def __init__(self,x,y,width,height):
        self.x = x
        self.y = y
        self.width = width
        self.height = height

    def __repr__(self):
        return "Rect({x}, {y}, {width}, {height})".format(**vars(self))

Now this is all well and good, but wouldn't it be convenient if we could immediately find the center. The center of the rectangle is of course dependent on the four primary attributes of the rectangle, and as such, just adding another attribute wouldn't quite accomplish what we want (the user might change any of the initial attributes making our center incorrect). The simplest way to solve this is to add a new method which uses the @property decorator.
Code: Select all
class Rect(object):
    def __init__(self,x,y,width,height):
        self.x = x
        self.y = y
        self.width = width
        self.height = height

    def __repr__(self):
        return "Rect({x}, {y}, {width}, {height})".format(**vars(self))

    @property
    def center(self):
        center_x = self.x + self.width/2.0
        center_y = self.y + self.height/2.0
        return center_x,center_y
Code: Select all
>>> r = Rect(0,0,50,50)
>>> r.center
(25.0, 25.0)
>>> r.width = 100
>>> r.center
(50.0, 25.0)
>>>

Well that is great; we can now use center as if it were a standard attribute. Let's try to assign to it:
Code: Select all
>>> r.center = (0,0)
Traceback (most recent call last):
  File "<interactive input>", line 1, in <module>
AttributeError: can't set attribute
>>>

Well... that didn't work. Our center method behaves like an attribute when we retrieve its value, but we can't assign to it (and rightly so). This brings us to the topic at hand; how do we create mutually dependent class attributes (as stated at the top, there are many ways to do this)?

First lets take a step back away from decorators and look at the property builtin. This builtin is a class that takes a getter, setter, deleter, and doc string in its __init__ (only one of the first three arguments is required).
Code: Select all
property(fget=None, fset=None, fdel=None, doc=None)
Please type help(property) for more detail

Let's take a look at how this works:
Code: Select all
class Rect(object):
    def get_center(self):
        return (self.x + self.width/2.0,self.y + self.height/2.0)
    def set_center(self,value):
        (self.x,self.y) = (value[0]- self.width/2.0, value[1]-self.height/2.0)

    center = property(get_center,set_center)

    def __init__(self,x,y,width,height):
        self.x = x
        self.y = y
        self.width = width
        self.height = height

    def __repr__(self):
        return "Rect({x}, {y}, {width}, {height})".format(**vars(self))
Code: Select all
>>> r = Rect(0,0,50,50)
>>> r.center
(25.0, 25.0)
>>> r2 = Rect(20,20,75,75)
>>> r2.center
(57.5, 57.5)
>>> r.center = 0,0
>>> r
Rect(-25.0,-25.0,50,50)
>>> r2
Rect(20,20,75,75)
>>>

So, this is working exactly as we wanted... but it is ugly as all hell. Imagine if we were doing this for numerous other properties of our rectangle. Let's now move back to the decorator approach and see if we can't make it look a little better.
Code: Select all
class Rect(object):
    def __init__(self,x,y,width,height):
        self.x = x
        self.y = y
        self.width = width
        self.height = height

    def __repr__(self):
        return "Rect({x}, {y}, {width}, {height})".format(**vars(self))

    @property
    def center(self):
        return (self.x + self.width/2.0,self.y + self.height/2.0)
    @center.setter
    def center(self,value):
        (self.x,self.y) = (value[0]- self.width/2.0, value[1]-self.height/2.0)

This works identically to the previous example, and looks a bit cleaner. I still don't like it much though; we now appear to have two methods with the same name--something that normally wouldn't work.

This brings us to the new concept of descriptors. A descriptor is a class that has one or more of the methods __get__, __set__, or __delete__ overloaded. The following demonstrates:
Code: Select all
class _Center(object):
    def __get__(self, instance, owner):
        return instance.x + instance.width/2.0, instance.y + instance.height/2.0
    def __set__(self, instance, value):
        instance.x = value[0] - instance.width/2.0
        instance.y = value[1] - instance.height/2.0


class Rect(object):
    center = _Center()

    def __init__(self,x,y,width,height):
        self.x = x
        self.y = y
        self.width = width
        self.height = height

    def __repr__(self):
        return "Rect({x}, {y}, {width}, {height})".format(**vars(self))

I personally think that this ends up being one of the cleanest solutions to the problem. One method I haven't mentioned here is using __getattr__ and __setattr__ to do the same thing. It will often require a ridiculously long if/elif/else block. Certainly another way of implementing this behavior, and in simple cases I suppose it won't look too ugly.

-Mek
Last edited by Mekire on Fri Jul 26, 2013 6:40 am, edited 1 time in total.
User avatar
Mekire
 
Posts: 984
Joined: Thu Feb 07, 2013 11:33 pm
Location: Amakusa, Japan

Re: Classes [advanced]: Dependent attributes (and Descriptor

Postby micseydel » Fri Jul 26, 2013 6:00 am

Cool, thanks for sharing! By the way, I would replace your __repr__ body with this
Code: Select all
return "Rect({x}, {y}, {width}, {height})".format(**vars(self))
Join the #python-forum IRC channel on irc.freenode.net!
User avatar
micseydel
 
Posts: 1131
Joined: Tue Feb 12, 2013 2:18 am
Location: Mountain View, CA

Re: Classes [advanced]: Dependent attributes (and Descriptor

Postby Mekire » Fri Jul 26, 2013 6:45 am

Ahh, yes. That looks much cleaner; updated. I never remember the vars function exists.

-Mek
User avatar
Mekire
 
Posts: 984
Joined: Thu Feb 07, 2013 11:33 pm
Location: Amakusa, Japan

Re: Classes [advanced]: Dependent attributes (and Descriptor

Postby micseydel » Fri Jul 26, 2013 6:49 am

I love it, and locals() for having more semantic formatting strings. I'm surprised it isn't more common. Seriously though, kudos on the post!
Join the #python-forum IRC channel on irc.freenode.net!
User avatar
micseydel
 
Posts: 1131
Joined: Tue Feb 12, 2013 2:18 am
Location: Mountain View, CA


Return to Tutorials

Who is online

Users browsing this forum: No registered users and 1 guest