Unification of Methods and Functions

David MacQuigg dmq at gain.com
Sat May 29 18:11:50 EDT 2004


On 29 May 2004 16:31:38 GMT, Duncan Booth <me at privacy.net> wrote:

>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.

The information is duplicate only if the class we want to pass in is
the always the same as the class in which the method is defined, in
which case, we would just hard-code the class name in the method.

class Mammal(Animal):
    def cmeth(cls, *args):
        print cls, args
    cm = classmethod(cmeth)
    def smeth(cls, *args):
        print cls, args
    sm = staticmethod(smeth)

Using a classmethod:
>>> Mammal.cm()
<class '__main__.Mammal'> ()
>>> Feline.cm()
<class '__main__.Feline'> ()

Using a staticmethod:
>>> msm = Mammal.sm  # type "redundant" info only once
>>> msm(Mammal)
<class '__main__.Mammal'> ()
>>> msm(Feline)
<class '__main__.Feline'> ()

You are right, we could just ignore the cls argument in the
classmethod, and avoid the need to learn staticmethods, but between
the two, I would rather use staticmethod, as it is simpler in most
cases.

What would be nice is if we had a variable like __class__ that would
work in any method form.  Then we would not need a special first
argument for classmethods, or any special method form at all for this
purpose.

>> 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:

I like these examples, and I think they will fit nicely into the
teaching sequence -- Spam and Foo, Animals_1, Animals_2, then some
real programs.  I would change the classmethods to staticmethods,
however, and avoid the need to teach classmethods.  This would make
your calls look like:

print Shape.fromCenterAndSize(Rectangle, 10, 10, 3, 4)
  -- or if you prefer a short alias --
print CS(Rectangle, 10, 10, 3, 4)

-- Dave


>-------- 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