Wednesday, March 26, 2014

Really Understanding Python @staticmethod and @classmethod

Nowadays people don't write blog posts explaining subtle programming concepts. They write surprisingly well-versed posts on StackOverflow which I personally use a lot. But in this case, I noticed the answers to the question "What is the difference between @staticmethod and @classmethod in Python?" are unsatisfactory. That gave me the excuse to write another blog post.

Python has three types of methods. Here is an example illustrating their use in Python.
class MyClass(object):

    def some_instance_method(self, *args, **kwds):
        pass

    @classmethod
    def some_class_method(cls, *args, **kwds):
        pass

    @staticmethod
    def some_static_method(*args, **kwds):
        pass
Python is unique in that its instance methods take an explicit instance argument. You can call it any name, but the convention is to call it self. Many languages like C++ and Java have an implicit this instead. Syntax wise, the class method would take a class (which is also an object in Python) as its first argument, and a static method takes no explicit class nor object references.

Many people understand that static methods do not use per-instance object states, but they implement functionality grouped as part of the class. That's why you can forego the explicit references. But nobody seems to explain well what class methods are good for. Class states? That's a synonym to the dirty word "global variables" which everyone knows you should avoid. But class methods have legitimate uses.

The first legitimate use is to use a class method like a static method, but better.

Imagine you have a static method that wants to use another static method in the same class, or a static method that wants to use constants defined in the class. You could refer to the class by its full name like this:
class Smoothie(object):

    YOGURT = 1
    STRAWBERRY = 2
    BANANA = 4
    MANGO = 8

    @staticmethod
    def blend(*mixes):
        return sum(mixes) / len(mixes)

    @staticmethod
    def eternal_sunshine():
        return Smoothie.blend(
            Smoothie.YOGURT, Smoothie.STRAWBERRY,
            Smoothie.BANANA)

    @staticmethod
    def mango_lassi():
        return Smoothie.blend(
            Smoothie.YOGURT, Smoothie.MANGO)
But with class method, you could write instead:
class Smoothie(object):

    YOGURT = 1
    STRAWBERRY = 2
    BANANA = 4
    MANGO = 8

    @staticmethod
    def blend(*mixes):
        return sum(mixes) / len(mixes)

    @classmethod
    def eternal_sunshine(cls):
        return cls.blend(cls.YOGURT, cls.STRAWBERRY, cls.BANANA)

    @classmethod
    def mango_lassi(cls):
        return cls.blend(cls.YOGURT, cls.MANGO)
This results in a much more succinct code. Imagine the headache you would have saved renaming the class if you later decide that SimpleSmoothie is a better name for just Smoothie. Indeed, making code refactoring easier is one of the first use case I discovered with Python class methods.

This is what you get if you want to see what the class methods return:
>>> Smoothie.eternal_sunshine()
2
>>> Smoothie.mango_lassi()
4
There is a subtle performance benefit. If you refer to the full class name in a static method, the Python interpreter has to first look up the local variables, not finding it there, and then look up the global variables. With a class method, cls would be found in the local variables, so there is no need to look up the globals.

With this in mind, the second legitimate use is to allow a subclass to override static and class methods.
class BetterSmoothie(Smoothie):

    YOGURT = 'yogurt'
    STRAWBERRY = 'strawberry'
    BANANA = 'banana'
    MANGO = 'mango'

    @staticmethod
    def blend(*mixes):
        return ', '.join(mixes)
Now, without touching the class methods eternal_sunshine() and mango_lassi(), they will use the new blend() as well as the new constants, and produce a different kind of smoothie mix.
>>> BetterSmoothie.eternal_sunshine()
'yogurt, strawberry, banana'
>>> BetterSmoothie.mango_lassi()
'yogurt, mango'
If you use a class method for your class factory, then it will be able to construct derived classes too. It may or may not make sense depending on your class design.
class Xyzzy(object):

    def __init__(self, which_factory):
        self.which_factory = which_factory

    @classmethod
    def factory_foo(cls):
        return cls('factory_foo')

    @classmethod
    def factory_bar(cls):
        return cls('factory_bar')

class Yappy(Xyzzy):

    def __init__(self, which_factory):
        super(Yappy, self).__init__(which_factory)
        print which_factory
Notice that only the Yappy class prints out the factory type, but not Xyzzy. And this is what you get:
>>> Xyzzy.factory_foo()
<__main__.Xyzzy object at 0x1027d1050>
>>> Xyzzy.factory_bar()
<__main__.Xyzzy object at 0x1027c7e10>
>>> Yappy.factory_foo()
factory_foo
<__main__.Yappy object at 0x1027d10d0>
>>> Yappy.factory_bar()
factory_bar
<__main__.Yappy object at 0x1027c7e10>
With class method factories, even though the factories are defined in the base class, they are able to construct the derived classes as well.

Class method in Python is really a unique feature. Some use cases shown above demonstrate how to use class methods to refer to other static or class methods in the same class and how to allow a subclass to change behavior of the base class. The closest analog in C++ would be the curiously recurring template pattern.

1 comment:

Prognostico said...

class Xyzzy(object):

def __init__(self, which_factory):
self.which_factory = which_factory

@classmethod
def factory_foo(cls):
return cls('factory_foo')

@classmethod
def factory_bar(cls):
return cls('factory_bar')

class Yappy(Xyzzy):

def __init__(self, which_factory):
super(Yappy, self).__init__(which_factory)
print(which_factory)

fact_f = Xyzzy.factory_foo()
fact_b = Xyzzy.factory_bar()
yap_f = Yappy.factory_foo()
yap_b = Yappy.factory_bar()

'''
# CODE BELOW WAS TO DEMONSTRATE FOR MY SELF
# HOW I GET EVERYTHING

from time import sleep
class Xyzzy(object):

def __init__(self, which_factory):
self.which_factory = which_factory
print(self)
print(which_factory)
print('4 vezes')
sleep(5)

@classmethod
def factory_foo(cls):
print('fact_foo 2 vezes')
print(cls)
sleep(5)
return cls('factory_toast')

@classmethod
def factory_bar(cls):
print('fact_bar 2 vezes')
sleep(5)
return cls('factory_bar')

class Yappy(Xyzzy):

def __init__(self, which_factory):
print(self)
print(which_factory)
print('2 vezes')
sleep(5)
super(Yappy, self).__init__(which_factory)
print('depois de super')
print(which_factory)


#fact_f = Xyzzy.factory_foo()
#fact_b = Xyzzy.factory_bar()
yap_f = Yappy.factory_foo()
#yap_b = Yappy.factory_bar()
print(yap_f.which_factory)


* DOES 5 SECONDS DIDN'T HELPED

'''
'''
START
LINE - WTF?!
20 fact_f start as variable and call factory_foo on class 'Xyzzy'
1 init method __new__ with object fact_f Xyzzy(fact_f)
6 jump for the @classmethod
7 factory_foo(Xyxxy)
8 return Xyzzy('factory_foo')
3 __init__(fact_f, 'factory_foo')
4 fact_f.wich_factory = 'factory_foo'
20 fact_f is now just a object in memory with 'factory foo' as atribute
21 fact_b start as variable and call method factory_bar on class 'Xyzzy'
1 init method __new__ with object fact_b == Xyzzy(fact_b)
10 jump for the method using @classmethod
11 factory_bar(Xyxxy)
8 return Xyzzy('factory_foo')
3 __init__(fact_b, 'factory_bar')
4 fact_b.wich_factory = 'factory_bar'
21 fact_b is now just a object in memory with 'factory bar' as atribute
22 yap_f start as variable and call factory_foo with class Yappy
6
7 factory_foo(Yappy)
8 return Yappy('factory_foo')
# the method factory_foo return a class with one arg
14 start class Yappy with __new__ method, yap_f object
# then, when the class start with the new method, the self parameter is now the object, in this case 'yap_f'
16 start method constructor __init__(yap_f, 'factory_foo')
17 call class Xyzzy
1 Xyzzy(yap_f)
3 init(yap_f, 'factory_foo')
4 yap_f.which_factory = 'factory_foo'
17 yap_f now have the instatement 'which_factory' with string "factory_foo" and can be called any time
18 print('factory_foo')
22 yap_f returned as object and now we can print does atributes
23 yap_b start as variable and call factory_bar with class Yapp
10
11 factory_bar(Yappy)
12 return Yappy('factory_bar')
# the method factory_bar return a class with one arg
14 start class Yappy with __new__ method, yap_b object
# then, when the class start with the new method, the self parameter is now the object, in this case 'yap_b'
16 start method constructor __init__(yap_b, 'factory_foo')
17 call class Xyzzy
1 Xyzzy(yap_b)
3 init(yap_b, 'factory_bar')
4 yap_b.which_factory = 'factory_bar'
17 yap_b now have the instatement 'which_factory' with string "factory_bar" and can be called any time
18 print('factory_bar')
23 yap_b returned as object and now we can print does atributes
STOP
'''