Adding an interface to existing classes

S

Spencer Pearson

I'm writing a geometry package, with Points and Lines and Circles and
so on, and eventually I want to be able to draw these things on the
screen. I have two options so far for how to accomplish this, but
neither of them sits quite right with me, and I'd like the opinion of
comp.lang.python's wizened elders.

Option 1. Subclassing.
The most Pythonic way would seem to be writing subclasses for the
things I want to display, adding a ".draw(...)" method to each one,
like this:
class DrawablePoint( geometry.Point ):
class draw( self, ... ):
...

When the time comes to draw things, I'll have some list of objects I
want drawn, and say
for x in to_draw:
x.draw(...)

I see a problem with this, though. The intersection of two lines is
(usually) an object of type Point. Since DrawableLine inherits from
Line, this means that unless I redefine the "intersect" method in
DrawableLine, the intersection of two DrawableLines will be a Point
object, not a DrawablePoint. I don't want to saddle the user with the
burden of converting every method output into its corresponding
Drawable subclass, and I don't want to redefine every method to return
an instance of said subclass. I see no other options if I continue
down the subclassing path. Am I missing something?




Option 2. A "draw" function, with a function dictionary.
This feels weird, but is fairly simple to write, use, and extend. We
have a module with a "draw_functions" dictionary that maps types onto
functions, and a "draw" function that just looks up the proper type in
the dictionary and calls the corresponding function. If you create
your own object, you can just add a new entry to the dictionary. The
implementation is simple enough to outline here:

In file "geometry/gui.py":
def draw_point(...):
...
def draw_line(...):
...
draw_functions = {geometry.Point: draw_point, geometry.Line:
draw_line, ...}
def draw( x, *args, **kwargs ):
for type, callback in draw_functions.iteritems():
if isinstance(x, type):
callback(x, *args, **kwargs)
else:
raise TypeError("don't know how to draw things of type "
"{0}".format(type(x)))


In the file that uses this:
# Drawing a predefined type of object:
geometry.gui.draw(some_point, ...)
# Here we define a new kind of object and tell the package how to draw
it.
class MyObject(GeometricObject):
...
def draw_my_object(...):
...
geometry.gui.draw_functions[MyObject] = draw_my_object
# And now we draw it.
geometry.gui.draw(MyObject(...), ...)


If I feel fancy, I might use a decorator for adding entries to
draw_functions, but this is basically how it'd work.




The second way feels kludgey to me, but I'm leaning towards it because
it seems like so much less work and I'm out of ideas. Can anyone help,
explaining either a different way to do it or why one of these isn't
as bad as I think?

Thanks for your time!
-Spencer
 
T

Terry Reedy

I'm writing a geometry package, with Points and Lines and Circles and
so on, and eventually I want to be able to draw these things on the
screen. I have two options so far for how to accomplish this, but
neither of them sits quite right with me, and I'd like the opinion of
comp.lang.python's wizened elders.

Option 1. Subclassing.
The most Pythonic way would seem to be writing subclasses for the
things I want to display, adding a ".draw(...)" method to each one,
like this:
Option 2. A "draw" function, with a function dictionary.

Option 3? Add a draw method to existing classes, rather than subclassing?
 
S

Spencer Pearson

Option 3? Add a draw method to existing classes, rather than subclassing?

Thanks for the response! Do you mean something like this?
class Point(GeometricObject):
def intersect(self, other):
...
def union(self, other):
...
def draw(self, ...):
...

I'd like to avoid this, because... well, what I want is a geometry
package, and it seems to me that whistles and bells like GUI support
ought to be confined to subpackages. I'd look at this and think, "the
rest of the Point class deals with fundamental geometric reality.
What's this GUI method doing mixed in with my beautiful mathematical
purity?" Is this the wrong attitude to take?

Or did you mean this?
In file "geometry/gui.py":
def draw_point(point, ...):
...
Point.draw = draw_point

I've never modified an existing class before, and I fear the
unfamiliar. If that's what you meant... it's really an acceptable
thing to do? It seems like somebody might see "some_point.draw(...)"
and be confused by the method's absence in the Point class definition.

(A hybrid approach would be to say "draw = geometry.gui.draw_point"
inside the body of the Point class, setting the attribute there
instead of in gui.py, but that makes me uneasy for the same reason as
putting the function's full definition in the class.)

Thanks again,
-Spencer
 
I

Ian Kelly

I see a problem with this, though. The intersection of two lines is
(usually) an object of type Point. Since DrawableLine inherits from
Line, this means that unless I redefine the "intersect" method in
DrawableLine, the intersection of two DrawableLines will be a Point
object, not a DrawablePoint. I don't want to saddle the user with the
burden of converting every method output into its corresponding
Drawable subclass, and I don't want to redefine every method to return
an instance of said subclass. I see no other options if I continue
down the subclassing path. Am I missing something?

You could solve this with a factory. Instead of having
Line.intersection() create a Point directly, have it call
self.factory.create_point(). In the drawing module, replace the
factory for the subclasses with one that creates drawable classes
instead.

Option 2. A "draw" function, with a function dictionary.
This feels weird, but is fairly simple to write, use, and extend. We
have a module with a "draw_functions" dictionary that maps types onto
functions, and a "draw" function that just looks up the proper type in
the dictionary and calls the corresponding function. If you create
your own object, you can just add a new entry to the dictionary. The
implementation is simple enough to outline here:

This will also work, but inheritance complicates things a little. Do
you do a type equality check or an isinstance() check when looking up
the type in the dictionary? If the former, then somebody who
subclasses geometry.Line to create a GroovyLine subclass must add an
entry for GroovyLine to the dictionary, even if the drawing code is
the same. The drawing code is not inherited.

But if you do an isinstance() check, then you need to be very careful
about how you check it. If somebody creates a WavyLine subclass and
registers a different drawing method, then you need to make sure the
isinstance() check for the WavyLine implementation happens before the
regular Line implementation, or the wrong drawing code may get invoked
for WavyLine. Probably the easiest way to do this correctly is to
follow the MRO that Python has conveniently already built for you.
Something like:

def draw(x, *args, **kwargs ):
for class_ in type(x).__mro__:
if class_ in draw_functions:
draw_functions[class_](*args, **kwargs)
break
else:
raise TypeError("don't know how to draw things of type "
"{0}".format(type(x)))

Cheers,
Ian
 
R

Rick Johnson

I'm writing a geometry package, with Points and Lines and Circles and
so on, and eventually I want to be able to draw these things on the
screen.

....which is the main reason for creating a geometry package in the
first place!. I mean, imaginary points and lines are great if you are
a theoretical mathematician, but for the rest of us earthlings,
plotted pixels are the "in thang" now-a-days.
I have two options so far for how to accomplish this, but
neither of them sits quite right with me, and I'd like the opinion of
comp.lang.python's wizened elders.

Option 1. Subclassing.
The most Pythonic way would seem to be writing subclasses for the
things I want to display, adding a ".draw(...)" method to each one,
like this:
class DrawablePoint( geometry.Point ):
    class draw( self, ... ):
        ...

"DrawablePoint"? I suppose there is an "ImaginaryPoint" somewhere in
this API? Geez
When the time comes to draw things, I'll have some list of objects I
want drawn, and say
for x in to_draw:
    x.draw(...)

Seems reasonable. Not the fastest approach, but a good start for a
very high level interface.
I see a problem with this, though. The intersection of two lines is
(usually) an object of type Point. Since DrawableLine inherits from
Line,

Why the hell is "Drawable" inheriting from Line? I would think that a
"Line" would be "Drawable" object and NOT vice-versa? Am i wrong?
this means that unless I redefine the "intersect" method in
DrawableLine, the intersection of two DrawableLines will be a Point
object, not a DrawablePoint.
OMFG!

I don't want to saddle the user with the
burden of converting every method output into its corresponding
Drawable subclass, and I don't want to redefine every method to return
an instance of said subclass. I see no other options if I continue
down the subclassing path. Am I missing something?

Yes, a proper object hierarchy and API it seems @_@.

Spencer, i would re-think this entire project from the beginning. You
are trying to make an object out of everything. You don't need to make
an object of EVERYTHING. Ask yourself, what are the most basic objects
of a geometry library, and then report back to us your findings.

PS: I prefer option1 for these things as the OOP paradigm fits nicely.
I just hate to have modules of loose functions just lying about.
 
T

Terry Reedy

There are people who would advocate a Drawable base class with a virtual
or abstract .draw method and that DrawablePoint, etc, inherit from
Drawable and Point.
Thanks for the response! Do you mean something like this?
class Point(GeometricObject):
def intersect(self, other):
...

I am interpreting this to mean that you have a world coordinate system
for instances that have location and size.
def union(self, other):
...
def draw(self, ...):
...

Yes. I would consider that Option 0, the default, unless you have good
reason to choose another. I would certainly include it on a list of options.
I'd like to avoid this, because... well, what I want is a geometry
package, and it seems to me that whistles and bells like GUI support
ought to be confined to subpackages. I'd look at this and think, "the
rest of the Point class deals with fundamental geometric reality.
What's this GUI method doing mixed in with my beautiful mathematical
purity?"

By default, all Python objects have a text representation method. I do
not see that giving all concrete geometric objects (with a location and
size) a visual representation is much different. I would use drawing
functions that accept the coordinates and distances of your geometry
world and translate to low-level pixel functions for a particular gui
system. I agree that your geometrical objects should not know about
pixels, screens, windows, and aspect ratios.
Is this the wrong attitude to take?

It depends on *your* goal and values.
Or did you mean this?
In file "geometry/gui.py":
def draw_point(point, ...):
...
Point.draw = draw_point

I've never modified an existing class before, and I fear the
unfamiliar. If that's what you meant... it's really an acceptable
thing to do?

Yes, in my opinion. The advantage of this is putting all the draw
methods together, and possibly having more than one one set. On the
other hand, one needs to know the data attributes of each class to
understand its draw method.
It seems like somebody might see "some_point.draw(...)"
and be confused by the method's absence in the Point class definition.

With either suboption, you should put an abstract .draw method in the
GeometricObject base class.

I would look at various game, graph, geometry, and gui packages handle
drawing for more ideas.
 
S

Steven D'Aprano

"DrawablePoint"? I suppose there is an "ImaginaryPoint" somewhere in
this API? Geez

No, but there's an abstract Point, which presumably refers to the
geometric concept (hence the module, geometry) without concerning itself
with such things as pixels, raster and vector output devices, screen
resolutions, and all the other stuff which is needed for drawing points
but not needed for working with points.


[...]
Why the hell is "Drawable" inheriting from Line? I would think that a
"Line" would be "Drawable" object and NOT vice-versa? Am i wrong?

Probably. I think there's a case for Drawable to be an abstract mixin
class, so that DrawableLine inherits from both Line and Drawable.


Not if you define intersect in Point correctly in the first place.

class Point: # An abstract class.
def intersect(self, other):
blah; blah; blah
return Point(x, y) # No, wrong, bad!!! Don't do this.

Instead:

return self.__class__(x, y) # Better.

Spencer, i would re-think this entire project from the beginning. You
are trying to make an object out of everything. You don't need to make
an object of EVERYTHING.

Very true.
 
C

Chris Angelico

class Point:  # An abstract class.
   def intersect(self, other):
       blah; blah; blah
       return Point(x, y)  # No, wrong, bad!!! Don't do this.

Instead:

       return self.__class__(x, y)  # Better.

This would work if you were dealing with the intersection of two
points, but how do you use that sort of trick for different classes?

ChrisA
 
S

Steven D'Aprano

This would work if you were dealing with the intersection of two points,
but how do you use that sort of trick for different classes?

There's nothing in the above that assumes that other has the same type as
self. It's just that the type of other is ignored, and the type of self
always wins. I find that a nice, clear rule: x.intersection(y) always
returns a point with the same type as x.

If you want a more complicated rule, you have to code it yourself:


def intersection(self, other):
if issubclass(type(other), type(self)):
kind = type(other)
elif issubclass(type(self), type(other)):
kind = AbstractPoint
elif other.__class__ is UserPoint:
kind = UserPoint
elif today is Tuesday:
kind = BelgiumPoint
else:
kind = self.__class__
return kind(x, y)
 
C

Chris Angelico

There's nothing in the above that assumes that other has the same type as
self. It's just that the type of other is ignored, and the type of self
always wins. I find that a nice, clear rule: x.intersection(y) always
returns a point with the same type as x.

The intersection of DrawableLine and DrawableLine is DrawablePoint.
That's not the same type as either of the inputs. Same if you seek the
intersection of two planes, which is a line - or two spheres, which is
a circle (with possible failure if they don't intersect).

ChrisA
 
S

Steven D'Aprano

The intersection of DrawableLine and DrawableLine is DrawablePoint.
That's not the same type as either of the inputs. Same if you seek the
intersection of two planes, which is a line - or two spheres, which is a
circle (with possible failure if they don't intersect).

class Line:
intersection_kind = Point
def intersection(self, other):
blah()
return self.intersection_kind(a, b)

class DrawableLine(Line):
intersection_kind = DrawablePoint
 
S

Spencer Pearson

(I'm sorry for my delayed response -- I've been travelling and not had
reliable Internet access.)

You could solve this with a factory. Instead of having
Line.intersection() create a Point directly, have it call
self.factory.create_point(). In the drawing module, replace the
factory for the subclasses with one that creates drawable classes
instead.

Oh, that's a neat idea. Yes, I think that does exactly what I want!
Thanks very much!
This will also work, but inheritance complicates things a
little. ...
... Probably the easiest way to do this correctly is to follow the
MRO that Python has conveniently already built for you. Something
like:

def draw(x, *args, **kwargs ):
for class_ in type(x).__mro__:
if class_ in draw_functions:
draw_functions[class_](*args, **kwargs)
break
else:
raise TypeError("don't know how to draw things of type "
"{0}".format(type(x)))

You're right, you're right. My implementation was sloppy. I think I'll
go with your factory solution, but thanks for the fixed version of
draw() -- I've never seen __mro__ before, and it seems to be just the
tool for this job!

-Spencer
 
S

Spencer Pearson

There are people who would advocate a Drawable base class with a virtual
or abstract .draw method and that DrawablePoint, etc, inherit from
Drawable and Point.


I am interpreting this to mean that you have a world coordinate system
for instances that have location and size.


Yes. I would consider that Option 0, the default, unless you have good
reason to choose another. I would certainly include it on a list of options.


By default, all Python objects have a text representation method. I do
not see that giving all concrete geometric objects (with a location and
size) a visual representation is much different. I would use drawing
functions that accept the coordinates and distances of your geometry
world and translate to low-level pixel functions for a particular gui
system. I agree that your geometrical objects should not know about
pixels, screens, windows, and aspect ratios.

 > Is this the wrong attitude to take?

It depends on *your* goal and values.



Yes, in my opinion. The advantage of this is putting all the draw
methods together, and possibly having more than one one set. On the
other hand, one needs to know the data attributes of each class to
understand its draw method.


With either suboption, you should put an abstract .draw method in the
GeometricObject base class.

I would look at various game, graph, geometry, and gui packages handle
drawing for more ideas.

(I'm sorry for my delayed response -- I've been travelling and not had
reliable Internet access.)

There are people who would advocate a Drawable base class with a virtual
or abstract .draw method and that DrawablePoint, etc, inherit from
Drawable and Point.

Yes... yes, that makes sense to me.
By default, all Python objects have a text representation method. I do
not see that giving all concrete geometric objects (with a location and
size) a visual representation is much different. I would use drawing
functions that accept the coordinates and distances of your geometry
world and translate to low-level pixel functions for a particular gui
system. I agree that your geometrical objects should not know about
pixels, screens, windows, and aspect ratios.

Ha! Oh, I've been being silly. I was going to claim that since there
is no standard Python GUI, I ought not chain myself to any one of the
candidates. Then I learned that Tkinter comes standard with Python.
Oops.

All right, now that I know that, the comparison to having a text
representation seems very reasonable. I'll definitely reconsider
making the draw() method a requirement for all GeometricObjects.
Yes, in my opinion. The advantage of this is putting all the draw
methods together, and possibly having more than one one set. On the
other hand, one needs to know the data attributes of each class to
understand its draw method.


With either suboption, you should put an abstract .draw method in the
GeometricObject base class.

Sure, sure. I'm comfortable with this way of doing things now. Thanks
so much for all your help!

-Spencer
 
S

Spencer Pearson

(I'm sorry for my delayed response -- I've been travelling and not had
reliable Internet access.)
Very true.

I'm not sure I understand. Surely you're not both saying that I
shouldn't write a Point class? Here's an expression I'd like very
much to be able to type:

sphere1.intersect(sphere2).rotated(angle, axis)

If I represented points with something already defined, maybe a tuple
or a numpy array, then that would risk that the spheres were tangent,
that their intersection was not a Circle (a GeometricObject, with a
..rotated method), but a point (a tuple, without one).

class Line:
intersection_kind = Point
def intersection(self, other):
blah()
return self.intersection_kind(a, b)

class DrawableLine(Line):
intersection_kind = DrawablePoint

If I objected that the intersection of two Lines might, once in a blue
moon, be a Line (because they're the same line), this seems like it
would edge towards the factory solution that Ian Kelly suggested
(i.e. not "return self.intersection_kind(...)", but "return
self.factory.create_point(...)" or something similar). Is that so?

-Spencer
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,744
Messages
2,569,484
Members
44,903
Latest member
orderPeak8CBDGummies

Latest Threads

Top