[Tutor] design of Point class

Steven D'Aprano steve at pearwood.info
Sat Aug 21 03:24:28 CEST 2010


On Sat, 21 Aug 2010 01:45:18 am Gregory, Matthew wrote:
> Hi all,
>
> I often struggle with object design and inheritance.  I'd like
> opinions on how best to design a Point class to be used in multiple
> circumstances.
>
> I typically deal with geographic (either 2D or 3D) data, yet there
> are occasions when I need n-dimensional points as well.  My thought
> was to create a superclass which was an n-dimensional point and then
> subclass that to 2- and 3-dimensional cases.  The rub to this is that
> in the n-dimensional case, it probably makes most sense to store the
> actual coordinates as a list whereas with the 2- and 3-D cases, I
> would want 'named' variables, such as x, y, z.

It would surprise me greatly if numpy didn't already have such a class.


Other than using numpy, probably the simplest solution is to just 
subclass tuple and give it named properties and whatever other methods 
you want. Here's a simple version:

class Point(tuple):
    def __new__(cls, *args):
        if len(args) == 1 and isinstance(args, tuple):
            args = args[0]
        for a in args:
            try:
                a+0
            except TypeError:
                raise TypeError('ordinate %s is not a number' % a)
        return super(Point, cls).__new__(cls, args)
    @property
    def x(self):
        return self[0]
    @property
    def y(self):
        return self[1]
    @property
    def z(self):
        return self[2]
    def dist(self, other):
        if isinstance(other, Point):
            if len(self) == len(other):
                sq_diffs = sum((a-b)**2 for (a,b) in zip(self, other))
                return math.sqrt(sq_diffs)
            else:
                raise ValueError('incompatible dimensions')
        raise TypeError('not a Point')
    def __repr__(self):
        return "%s(%r)" % (self.__class__.__name__, tuple(self))


class Point2D(Point):
    def __init__(self, *args):
        if len(self) != 2:
            raise ValueError('need exactly two ordinates')

class Point3D(Point):
    def __init__(self, *args):
        if len(self) != 3:
            raise ValueError('need exactly three ordinates')

These classes gives you:

* immutability;
* the first three ordinates are named x, y and z;
* any ordinate can be accessed by index with pt[3];
* distance is only defined if the dimensions are the same;
* nice string form;
* input validation.


What it doesn't give you (yet!) is:

* distance between Points with different dimensions could easily be
  defined just by removing the len() comparison. zip() will
  automatically terminate at the shortest input, thus projecting the
  higher-dimension point down to the lower-dimension point;
* other distance methods, such as Manhattan distance;
* a nice exception when you as for (say) pt.z from a 2-D point, instead
  of raising IndexError;
* point arithmetic (say, adding two points to get a third).

But you can't expect me to do all your work :)


An alternative would be to have the named ordinates return 0 rather than 
raise an error. Something like this would work:

    @property
    def y(self):
        try: return self[1]
        except IndexError: return 0




-- 
Steven D'Aprano


More information about the Tutor mailing list