I'd like to be able to call the Polygon constructor with a "sides"
argument and get a subclass instance if appropriate, e.g. I'd like to
call Polygon(sides=3) and get back a triangle, e.g. something like
class Polygon:
def __init__(sides=None):
if sides==3: return Triangle(...)
but of course class initializers in Python don't return values.
Is wanting that bad OO practice in the first place? Is there a way to
do it in Python? A way to do it in other OO languages? I'm not too
up on this type of thing.
The obvious workaround is to define a function that calls the appropriate
constructor, e.g.
def newpoly(sides):
if sides==3: return Triangle(...)
...
but this isn't so great, because it doesn't fit in with multiple base
classes:
class RotatingMixin:
def rotate(): ... # make object rotate on the screen
class RotatingPolygon(Polygon, RotatingMixin): pass
Ideally I'd like a definition like that to automatically result in
rotating triangles and rectangles as well as polygons. I think CLOS
and/or Flavors can do things like that, but I've never used them.
Yet another workaround is to create a container object that has a
polygon as an instance variable:
class PolygonWrapper:
def __init__(self,sides):
if sides==3: self.mypoly=Triangle(...)
elif sides==4: self.mypoly=Rectangle(...)
else: self.mypoly=Triangle
but now the wrapper class has to intercept all the operations that can
be done on any of the shapes, and pass them to the internal object.
Hmm, is something like
def __getattr__(self,sym): return getattr(self.mypoly,sym)
in the PolygonWrapper class a reasonable solution?
I'm wondering how you OO wizards deal with this type of question.
It would seem to come up pretty often in OO designs.
Regards from caveman C programming land,
--phr
A factory function like this is a standard solution that has been
discussed in several posts on this list. Don't knock it.
Terry J. Reedy
I'm not knocking it, I just don't see how to extend the class from
additional base classes without having to add more special code to
the factory function, which is against the OO spirit. Any thoughts?
I hope this is not the only answer you get, because I've been struggling
with the same problem. I really don't know if it's a good idea to use
the solution I present below.
Paul Rubin wrote:
>
> Say I have a class Polygon, with subclasses Rectangle (4 sides)
> and Triangle (3 sides) which have some special operations.
>
> I'd like to be able to call the Polygon constructor with a "sides"
> argument and get a subclass instance if appropriate, e.g. I'd like to
> call Polygon(sides=3) and get back a triangle, e.g. something like
> class Polygon:
> def __init__(sides=None):
> if sides==3: return Triangle(...)
>
> but of course class initializers in Python don't return values.
> The obvious workaround is to define a function that calls the appropriate
> constructor, e.g.
> def newpoly(sides):
> if sides==3: return Triangle(...)
> ...
> but this isn't so great, because it doesn't fit in with multiple base
> classes:
> class RotatingMixin:
> def rotate(): ... # make object rotate on the screen
>
> class RotatingPolygon(Polygon, RotatingMixin): pass
>
> Ideally I'd like a definition like that to automatically result in
> rotating triangles and rectangles as well as polygons.
You could use metaclasses to get exactly this.
Metaclasses (MC's) allow you to do the two things you're asking for:
1. Use the MC's __call__ to implement a factory that plays nice with
inheritance
2. Use the MC's __init__ to automatically make square and triangle
versions of the RotatingPolygon class
In 2.2 it would be something like:
class _Polygon(object):
def __init__(self, sides):
self.sides = sides
class _Triangle(_Polygon):
def __init__(self):
self.sides = 3
class _Square(_Polygon):
def __init__(self):
self.sides = 4
class _PolygonMetaclass(object):
def __init__(self, name, bases, attributes):
if bases == ():
# class Polygon
self._pclass = _Polygon
self._tclass = _Triangle
self._sclass = _Square
else:
# class decending from Polygon
self._pclass = type(name,
(bases[0]._pclass,)+bases[1:],
attributes)
self._tclass = type(name,
(bases[0]._tclass,)+bases[1:],
attributes)
self._sclass = type(name,
(bases[0]._sclass,)+bases[1:],
attributes)
def __call__(self, sides, *args, **kwargs):
if sides == 3:
return self._tclass(*args, **kwargs)
if sides == 4:
return self._sclass(*args, **kwargs)
return self._pclass(sides, *args, **kwargs)
class Polygon:
__metaclass__ = _PolygonMetaclass
t = Polygon(3)
s = Polygon(4)
p = Polygon(5)
print t.__class__, t.sides
print s.__class__, s.sides
print p.__class__, p.sides
class RotationMixin(object):
pass
class RotatingPolygon(Polygon, RotationMixin):
pass
t = RotatingPolygon(3)
s = RotatingPolygon(4)
p = RotatingPolygon(5)
print t.__class__.__bases__, t.sides
print s.__class__.__bases__, s.sides
print p.__class__.__bases__, p.sides
Running this gives me:
<type '__main__._Triangle'> 3
<type '__main__._Square'> 4
<type '__main__._Polygon'> 5
(<type '__main__._Triangle'>, <type '__main__.RotationMixin'>) 3
(<type '__main__._Square'>, <type '__main__.RotationMixin'>) 4
(<type '__main__._Polygon'>, <type '__main__.RotationMixin'>) 5
I think most people would consider your proposed PolygonWrapper with
delegation through __getattr__ a very nice alternative ;)
"Half of what I say is nonsense. Unfortunately I don't know which half"
Paul Rubin wrote:
> > A factory function like this is a standard solution that has been
> > discussed in several posts on this list. Don't knock it.
>
> I'm not knocking it, I just don't see how to extend the class from
> additional base classes without having to add more special code to
> the factory function, which is against the OO spirit. Any thoughts?
I would use the factory funtion approach:
# polygon_test
class Polygon:
def identify(self):
print self.__class__
class Triangle(Polygon):
def triangle_method(self):
pass
class Rectangle(Polygon):
def rectangle_method(self):
pass
def factory(sides=None):
special_polygons = {3: Triangle,
4: Rectangle}
if sides in special_polygons.keys():
return special_polygons[sides]()
else:
return Polygon()
Python 2.1.1 (#0, Aug 5 2001, 15:48:16) [EMX GCC 2.8.1] on os2emx
Type "copyright", "credits" or "license" for more information.
>>> import polygon_test
>>> p = polygon_test.factory()
>>> p.identify()
polygon_test.Polygon
>>> t = polygon_test.factory(3)
>>> t.identify()
polygon_test.Triangle
>>> t.triangle_method()
>>>
> The obvious workaround is to define a function that calls the appropriate
> constructor, e.g.
> def newpoly(sides):
> if sides==3: return Triangle(...)
> ...
> but this isn't so great, because it doesn't fit in with multiple base
> classes:
> class RotatingMixin:
> def rotate(): ... # make object rotate on the screen
>
> class RotatingPolygon(Polygon, RotatingMixin): pass
>
> Ideally I'd like a definition like that to automatically result in
> rotating triangles and rectangles as well as polygons. I think CLOS
Your first model,
> class Polygon:
> def __init__(sides=None):
> if sides==3: return Triangle(...)
wouldn't do that either, even if
> but of course class initializers in Python don't return values.
would not be true. You would have:
Polygon
/ | \
Triangle | RotatingPolygon
|
Rectangle
so Triangle and Rectangle wouldn't inherit the rotate method. However, you
could add the rotate method directly to the Polygon class. (Chuck Esterbrook
has written an article about this in Linux Journal, if I remember correctly.)
And in that case, the factory function would also do the job.
Stefan
It would seem that you would have to write special code for additional
base classes anyway, since if you had something like:
class Polygon:
def __init__(self, sides):
if sides == 3:
return Triangle()
elif sides == 4:
return Rectangle()
return self
then you would need to re-write the __init__ method of a new, related
class (like RotatedPolygon, which adds a Rotated mixin class to the
Polygon class) to return the new base classes (RotatedTriagnle and
RotatedSquare). This seems like the same amount of work as re-writing a
module factory function for the new situation.
One way to get what you want is to keep information about the special-case
subclasses in a class variable of the base class, something like this:
class Polygon:
pass
class Rectangle(Polygon):
pass
class Triangle(Polygon):
pass
Polygon.special_cases = {3: Triangle, 4: Rectangle}
class Rotated:
pass
class RotatedPolygon(Rotated, Polygon):
pass
class RotatedRectangle(Rotated, Rectangle):
pass
class RotatedTriangle(Rotated, Triangle):
pass
RotatedPolygon.special_cases = {3: RotatedTriangle, 4: RotatedRectangle}
def newpolygon(sides, base=Polygon):
polyclass = base.special_cases.get(sides, base)
return polyclass(sides)
This allows the factory function to determine what subclass it should use
by passing it the appropriate base class. If you need more sophisticated
logic to determine what base class you want to use, you could instead hold
the special cases as class variables and put the logic in the factory
function:
Polygon.Triangle = Triangle
Polygon.Rectangle = Rectangle
RotatedPolygon.Triangle = RotatedTriangle
RotatedPolygon.Rectangle = RotatedRectangle
def newpolygon(sides, base=Polygon):
if sides == 3:
return base.Triangle()
elif sides == 4:
return base.Rectangle()
return base(sides)
mytriangle = newpolygon(3, RotatedPolygon)
Notice that most of the power of this approach flows from the fact that
everything is an object in Python, and so you can sling around and store
classes and functions just as easily as you can numbers and strings.
I hope that this gives you some ideas for how you can elegantly approach
the situation in Python.
channelling-an-OO-spiritly-yrs,
Corran
I've got a possibly sick solution. Assuming that the only parameter you're
dealing with is the sides, then you can use a dictionary and the fact that
classes are callable objects, something like this:
class GenericPolygon:
pass
class Triangle:
pass
class Rectangle:
pass
def newPolygon( sides ):
lookup = { 3 : Triangle, 4 : Rectangle }
creator = lookup.get( sides, GenericPolygon )
return creator()
Then to update your class list all you need to do is add new entries to
lookup. If you've got more complex criteria (more than one parameter, for
example) you can either a) if the criteria are simple constants you can use
tuples as the dict keys rather than numbers or b) replace lookup with a list
of predicate functions that test if the parameters match criteria for each
class.
-Chris
The thing is, that's exactly what a factory function is for - it does
whatever
special case logic is required to get the right class instantiated, so that
the code doesn't have to be replicated elsewhere in the system.
John Roth
Yes, however it doesn't completely solve the problem. Roeland's
post using metaclasses does what I need--all I have to do now is
spend a few more hours poring over his post trying to understand it :).
I'm not sure if I understand everything you are looking for. But here is
an example of how to do instance creation using the new class methods in
Python 2.2 (using a Smalltalk-ish OO idiom). The basic idea is to use
class methods for your factory functions. The *big* advantage to using
class methods (for factory functions) instead of using module functions is
that you can define "subclass" factory functions and take advantage of
inheritance and method override.
Hope this helps.
Jim
==================================
class Polygon:
subclassDict = {}
## class methods
def registerSubclass(myclass,classObj,sides):
myclass.subclassDict[sides] = classObj
registerSubclass = classmethod(registerSubclass)
def getSubclassWithSides(myclass,sides): # we will override this in
subclasses
return myclass.subclassDict.get(sides)
getSubclassWithSides = classmethod(getSubclassWithSides)
def newInstanceWithSides(myclass,sides,*pargs,**kargs): # main factory
function
instance = None
classObj = myclass.getSubclassWithSides(sides)
if classObj:
instance = classObj(*pargs,**kargs)
return instance
newInstanceWithSides = classmethod(newInstanceWithSides)
## add instance methods here if needed
class Triangle: pass
Polygon.registerSubclass(Triangle,3)
class Square: pass
Polygon.registerSubclass(Square,4)
class Rotating: pass
class RotatingPolygon(Polygon,Rotating):
## class methods
def getSubclassWithSides(myclass,sides): # override superclass class
method
classObj = Polygon.getSubclassWithSides(sides)
if classObj:
class mixinClass(classObj,Rotating): pass
mixinClass.__name__ = classObj.__name__ + Rotating.__name__ #
so that print will verify
classObj = mixinClass
return classObj
getSubclassWithSides = classmethod(getSubclassWithSides)
## add instance methods here if needed
triangle = Polygon.newInstanceWithSides(3)
print triangle
square = Polygon.newInstanceWithSides(4)
print square
rotatingTriangle = RotatingPolygon.newInstanceWithSides(3)
print rotatingTriangle
rotatingSquare = RotatingPolygon.newInstanceWithSides(4)
print rotatingSquare
C:\>python
Python 2.2a1 (#21, Jul 18 2001, 04:25:46) [MSC 32 bit (Intel)] on win32
Type "copyright", "credits" or "license" for more information.
>>>
>>>
>>> import example1
<example1.Triangle instance at 007FA8CC>
<example1.Square instance at 007FA77C>
<example1.TriangleRotating instance at 007FA67C>
<example1.SquareRotating instance at 007FA1CC>
>>>