I haven't added any classmethod examples to my OOP chapter, because
until now I've thought of them as very specialized. I'm searching for
a good textbook example, but all I can find is trivially replacable
with an instance method or a static method. If you have an instance
already, the class can be resolved via self.__class__. If you don't
have an instance, the desired class can be passed as an argument to a
static method.
I find that slightly surprising. You say that if you have a static method
you can pass the class as a parameter, but since you already specify the
class somewhere to access the static method surely you are duplicating
information unneccessarily? You could equally say that static methods
aren't needed because you can always use a class method and just ignore the
class parameter if you don't need it.
I sounds like you may have a good use case for classmethods. Could
you give us an example, and a brief explanation of what it does that
can't be done as easily with other method forms? Your help will be
greatly appreciated.
Ok, I'll try and give you a couple of examples, feel free to tear them
apart. The most obvious one is to write factory methods:
-------- begin cut ---------------
class Shape(object):
def __init__(self):
super(Shape, self).__init__()
self.moveTo(0, 0)
self.resize(10, 10)
def __repr__(self):
return "<%s instance at %s x=%s y=%s width=%s height=%s>" % (
self.__class__.__name__, id(self),
self.x, self.y, self.width, self.height)
def moveTo(self, x, y):
self.x, self.y = x, y
def resize(self, width, height):
self.width, self.height = width, height
# Factory methods
def fromCenterAndSize(cls, cx, cy, width, height):
self = cls()
self.moveTo(cx, cy)
self.resize(width, height)
return self
fromCenterAndSize = classmethod(fromCenterAndSize)
def fromTLBR(cls, top, left, bottom, right):
self = cls()
self.moveTo((left+right)/2., (top+bottom)/2.)
self.resize(right-left, top-bottom)
return self
fromTLBR = classmethod(fromTLBR)
class Rectangle(Shape): pass
class Ellipse(Shape): pass
print Rectangle.fromCenterAndSize(10, 10, 3, 4)
print Ellipse.fromTLBR(20, 0, 0, 30)
squares = [ Rectangle.fromCenterAndSize(i, j, 1, 1)
for i in range(2) for j in range(2) ]
print squares
-------- end cut ------------
Running this code gives something like:
<Rectangle instance at 9322032 x=10 y=10 width=3 height=4>
<Ellipse instance at 9322032 x=15.0 y=10.0 width=30 height=20>
[<Rectangle instance at 9321072 x=0 y=0 width=1 height=1>, <Rectangle
instance at 9320016 x=0 y=1 width=1 height=1>, <Rectangle instance at
9321200 x=1 y=0 width=1 height=1>, <Rectangle instance at 9321168 x=1 y=1
width=1 height=1>]
The important point is that the factory methods create objects of the
correct type.
The shape class has two factory methods here. Either one of them could
actually be moved into the initialiser, but since they both take the same
number and type of parameters any attempt to move them both into the
initialiser would be confusing at best. the factory methods give me two
clear ways to create a Shape, and it is obvious from the call which one I
am using.
Shape is clearly intended to be a base class, so I created a couple of
derived classes. Each derived class inherits the factory methods, or can
override them if it needs. Instance methods won't do here, as you want a
single call to create and initialise the objects. Static methods won't do
as unless you duplicated the class in the call you can't create an object
of the appropriate type.
My second example carries on from the first. Sometimes you want to count or
even find all existing objects of a particular class. You can do this
easily enough for a single class using weak references and a static method
to retrieve the count or the objects, but if you want to do it for several
classes, and want to avoid duplicating the code, class methods make the job
fairly easy.
--------- begin cut -------------
from weakref import WeakValueDictionary
class TrackLifetimeMixin(object):
def __init__(self):
cls = self.__class__
if '_TrackLifetimeMixin__instances' not in cls.__dict__:
cls.__instances = WeakValueDictionary()
cls.__instancecount = 0
cls.__instances[id(self)] = self
cls.__instancecount += 1
def __getInstances(cls):
return cls.__dict__.get('_TrackLifetimeMixin__instances' , {})
__getInstances = classmethod(__getInstances)
def getLiveInstances(cls):
instances = cls.__getInstances().values()
for k in cls.__subclasses__():
instances.extend(k.getLiveInstances())
return instances
getLiveInstances = classmethod(getLiveInstances)
def getLiveInstanceCount(cls):
count = len(cls.__getInstances())
for k in cls.__subclasses__():
count += k.getLiveInstanceCount()
return count
getLiveInstanceCount = classmethod(getLiveInstanceCount)
def getTotalInstanceCount(cls):
count = cls.__dict__.get('_TrackLifetimeMixin__instancecount' , 0)
for k in cls.__subclasses__():
count += k.getTotalInstanceCount()
return count
getTotalInstanceCount = classmethod(getTotalInstanceCount)
class Shape(TrackLifetimeMixin, object):
def __init__(self):
super(Shape, self).__init__()
self.moveTo(0, 0)
self.resize(10, 10)
def __repr__(self):
return "<%s instance at %s x=%s y=%s width=%s height=%s>" % (
self.__class__.__name__, id(self),
self.x, self.y, self.width, self.height)
def moveTo(self, x, y):
self.x, self.y = x, y
def resize(self, width, height):
self.width, self.height = width, height
# Factory methods
def fromCenterAndSize(cls, cx, cy, width, height):
self = cls()
self.moveTo(cx, cy)
self.resize(width, height)
return self
fromCenterAndSize = classmethod(fromCenterAndSize)
def fromTLBR(cls, top, left, bottom, right):
self = cls()
self.moveTo((left+right)/2., (top+bottom)/2.)
self.resize(right-left, top-bottom)
return self
fromTLBR = classmethod(fromTLBR)
class Rectangle(Shape): pass
class Ellipse(Shape): pass
print Rectangle.fromCenterAndSize(10, 10, 3, 4)
print Ellipse.fromTLBR(20, 0, 0, 30)
squares = [ Rectangle.fromCenterAndSize(i, j, 1, 1) for i in range(2) for j
in range(2) ]
print Shape.getLiveInstances()
for cls in Shape, Rectangle, Ellipse:
print cls.__name__, "instances:", cls.getLiveInstanceCount(), \
"now, ", cls.getTotalInstanceCount(), "total"
--------- end cut -------------
The middle part of this file is unchanged. I've added a new mixin class at
the top, but the class Shape is unchanged except that it now includes the
mixin class in its bases. The last 4 lines are also new and print a few
statistics about the classes Shape, Rectangle, Ellipse:
<Rectangle instance at 9376016 x=10 y=10 width=3 height=4>
<Ellipse instance at 9376016 x=15.0 y=10.0 width=30 height=20>
[<Rectangle instance at 9376240 x=0 y=0 width=1 height=1>, <Rectangle
instance at 9376272 x=0 y=1 width=1 height=1>, <Rectangle instance at
9376336 x=1 y=1 width=1 height=1>, <Rectangle instance at 9376304 x=1 y=0
width=1 height=1>]
Shape instances: 4 now, 6 total
Rectangle instances: 4 now, 5 total
Ellipse instances: 0 now, 1 total