Python Forum
[Classes] Classes [advanced]: Dependent attributes (and Descriptors)
Thread Rating:
  • 2 Vote(s) - 3 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[Classes] Classes [advanced]: Dependent attributes (and Descriptors)
#1
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. 
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.
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
>>> 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:
>>> 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).  
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:
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))
>>> 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.
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:
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
#2
This message left intentionally blank.


Possibly Related Threads…
Thread Author Replies Views Last Post
  [Classes] Classes [advanced]: Descriptors (managed attributes) Mekire 1 5,578 Oct-08-2016, 04:26 PM
Last Post: Kebap

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020