Unification of Methods and Functions

Duncan Booth me at privacy.net
Sat May 29 12:31:38 EDT 2004


David MacQuigg <dmq at gain.com> wrote in 
news:o1veb09o0vgiaf9b7cs4umu79jf977s5qa at 4ax.com:

> 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




More information about the Python-list mailing list