Python - can I programmatically decorate class methods from a class instance?

jononomo picture jononomo · Dec 31, 2012 · Viewed 8.4k times · Source

I have an object hierarchy in which almost all of the methods are class methods. It looks like the following:

class ParentObject(object):
    def __init__(self):
        pass

    @classmethod
    def smile_warmly(cls, the_method):
        def wrapper(kls, *args, **kwargs):
            print "-smile_warmly - "+kls.__name__
            the_method(*args, **kwargs)
        return wrapper

    @classmethod
    def greetings(cls):
        print "greetings"

class SonObject(ParentObject):
    @classmethod
    def hello_son(cls):
        print "hello son"

    @classmethod
    def goodbye(cls):
        print "goodbye son"

class DaughterObject(ParentObject):
    @classmethod
    def hello_daughter(cls):
        print "hello daughter"

    @classmethod
    def goodbye(cls):
        print "goodbye daughter"

if __name__ == '__main__':
    son = SonObject()
    son.greetings()
    son.hello_son()
    son.goodbye()
    daughter = DaughterObject()
    daughter.greetings()
    daughter.hello_daughter()
    daughter.goodbye()

The code as given outputs the following:

greetings
hello son
goodbye son
greetings
hello daughter
goodbye daughter

I would like the code to output the following:

-smile_warmly - SonObject
greetings
-smile_warmly - SonObject
hello son
-smile_warmly - SonObject
goodbye son
-smile_warmly - DaughterObject
greetings
-smile_warmly - DaughterObject
hello daughter
-smile_warmly - DaughterObject
goodbye daughter

But I don't want to add the line @smile_warmly before each method (and when I try to do that in the code above, I get error message TypeError: 'classmethod' object is not callable). Rather, I would like the decoration of each method to take place programmatically in the __init__() method.

Is it possible to programmatically decorate methods in Python?

EDIT: Found something that seems to work -- see my answer below. Thanks to BrenBarn.

Answer

BrenBarn picture BrenBarn · Dec 31, 2012

All a decorator does is return a new function. This:

@deco
def foo():
    # blah

is the same as this:

def foo():
    # blah
foo = deco(foo)

You can do the same thing whenever you like, without the @ syntax, just by replacing functions with whatever you like. So in __init__ or wherever else, you could loop through all the methods and for each one replace it with smilewarmly(meth).

However, instead of doing it in __init__, it would make more sense to do it when the class is created. You could do this with a metaclass, or more simply with a class decorator:

def smileDeco(func):
    def wrapped(*args, **kw):
        print ":-)"
        func(*args, **kw)
    return classmethod(wrapped)

def makeSmiley(cls):
    for attr, val in cls.__dict__.iteritems():
        if callable(val) and not attr.startswith("__"):
            setattr(cls, attr, smileDeco(val))
    return cls

@makeSmiley
class Foo(object):
    def sayStuff(self):
        print "Blah blah"

>>> Foo().sayStuff()
:-)
Blah blah

In this example I put the classmethod decoration inside my smileDeco decorator. You could also put it in makeSmiley so that makeSmiley returned smileDeco(classmethod(val)). (Which way you want to do it depends on how closely linked the smile-decorator is to the things being classmethods.) This means you don't have to use @classmethod inside the class.

Also, of course, in the loop in makeSmiley you can include whatever logic you like to decide (for instance, based on the method's name) whether to wrap it with the smile behavior or not.

Note that you'd have to be a little more careful if you really want to manually use @classmethod inside the class, because classmethods as accessed via the class __dict__ aren't callable. So you'd have to specifically check whether the object is a classmethod object, instead of just checking whether it's callable.