22

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.

tadasajon
  • 14,276
  • 29
  • 92
  • 144

2 Answers2

36

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.

BrenBarn
  • 242,874
  • 37
  • 412
  • 384
  • This looks very helpful. I'm trying to get it to work right now and will provide an update here if I can pull it off. – tadasajon Dec 31 '12 at 01:21
  • If I have dozens of classes that subclass my parent class, then I have to remember to decorate each class. I'd prefer to just have the logic that adjusts the methods happen automatically in the `__init__()` method. My child classes will not override __init__(), so the `__init__()` defined in the parent class will always be called when a child class is constructed. If I could place similar logic there, then I'd have the functionality I desire. So that's what I'm working on. – tadasajon Dec 31 '12 at 01:30
  • @JonCrowell: You can do that, but `__init__` is called for every *instance*, not every class, so it doesn't seem like what you want. If you want the behavior to be fully automatic you'll have to use a metaclass. You can read up on metaclasses [here](http://docs.python.org/2/reference/datamodel.html#customizing-class-creation) and [here](http://stackoverflow.com/questions/100003/what-is-a-metaclass-in-python). – BrenBarn Dec 31 '12 at 01:50
  • I have a class hierarchy in which I would like all methods to be class methods. I almost have what I want, thanks to your pointers. See my edit to my original question. I'll mark your answer as correct. – tadasajon Dec 31 '12 at 01:54
  • See my answer below, which produces the output I desire. – tadasajon Dec 31 '12 at 02:21
1

This solution produces the output I desire:

class ParentObject(object):
    def __init__(self):
        self._adjust_methods(self.__class__)

    def _adjust_methods(self, cls):
        for attr, val in cls.__dict__.iteritems():
            if callable(val) and not attr.startswith("_"):
                setattr(cls, attr, self._smile_warmly(val))
        bases = cls.__bases__
        for base in bases:
            self._adjust_methods(base)

    def _smile_warmly(self, the_method):
        def _wrapped(self, *args, **kwargs):
            print "-smile_warmly - " +self.__name__
            the_method(self, *args, **kwargs)
        cmethod_wrapped = classmethod(_wrapped)
        # cmethod_wrapped.adjusted = True
        return cmethod_wrapped

    def greetings(self):
        print "greetings"

class SonObject(ParentObject):
    def hello_son(self):
        print "hello son"

    def goodbye(self):
        print "goodbye son"

class DaughterObject(ParentObject):
    def hello_daughter(self):
        print "hello daughter"

    def goodbye(self):
        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 output is:

-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
tadasajon
  • 14,276
  • 29
  • 92
  • 144
  • This will cause problems if you instantiate a class more than once, since it will then wrap the already-wrapped methods again. At the least, you should set a flag on each class as you wrap it, to mark it as already wrapped. Then you could check the flag at the beginning and skip the wrapping if the class is already wrapped. – BrenBarn Dec 31 '12 at 03:20
  • Hmm - I think I might have screwed up. My toy example given here gives the output I want, but my real code, which is more complicated, is giving me a bewildering `TypeError: 'NoneType' object is not callable` message and crashing. – tadasajon Dec 31 '12 at 04:04
  • You might want to post a new question with that issue. – BrenBarn Dec 31 '12 at 04:33
  • Yes -- even now I'm creating a nice example to isolate the issue. :) – tadasajon Dec 31 '12 at 04:56