larkost

Python Class-based decorators don’t work on classes

There are two different ways of creating decorators: Classes or functions. Unfortunately, the Classes form does not work when you are trying to use it on Object methods (on functions it is fine). An example:

import inspect

class MyDecorator(object):
    func = None
    def __init__(self, *args, **kwargs):
        print('Init args: %r kwargs: %r' % (args, kwargs))
        if args and callable(args[0]):
            print('  Is method: %s' % inspect.ismethod(args[0]))
            self.func = args[0]

    def __call__(self, *args, **kwargs):
        print('Call func: %r args: %r kwargs: %r' % (self.func, args, kwargs))
        if self.func:
            return self.func(*args, **kwargs)  # no access to the proper `self`
        else:
            return args[0]  # note: return a wrapper func if you need more

class MyClass(object):
    color = 'Not set'

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

    @MyDecorator  # without parentheses
    def green(self):
        print('Green worked: %s' % self.color)

    @MyDecorator()  # with parentheses, but without any value(s)
    def red(self):
        print('Red worked: %s' % self.color)

    @MyDecorator('a')  # with parentheses and value(s)
    def blue(self):
        print('Blue worked: %s' % self.color)
    
    print('---- Parsing complete ----')


a = MyClass('orange')
try:
    a.green()
except Exception as e:
    print('Green failed: %s' % e)  # this gets called here
try:
    a.red()
except Exception as e:
    print('Red failed: %s' % e)
try:
    a.blue()
except Exception as e:
    print('Blue failed: %s' % e)

The output:

Init args: (<function green at 0x7f8db2957230>,) kwargs: {}
  Is method: False
Init args: () kwargs: {}
Call func: None args: (<function red at 0x7f8db29572a8>,) kwargs: {}
Init args: ('a',) kwargs: {}
Call func: None args: (<function blue at 0x7f8db2957320>,) kwargs: {}
---- Parsing complete ----
Call func: <function green at 0x7f8db2957230> args: () kwargs: {}
Green failed: green() takes exactly 1 argument (0 given)
Red worked: orange
Blue worked: orange

This is a bit much to digest all at once, but a point summary of what is going on, first for red and blue, which work fine:

For the green path (without parentheses) things are very different:

Summary

This is only going to affect you if you use a) Class-style decorators, b) on Object methods, c) without using parentheses. But it is still an annoying problem. I am seeing this on Python 2.7.6, and 3.4.3.

All of this seems overly-complicated, and non-Pythonic to me. But if you want decorators that work in all of these cases, use my universal decorator example as a starting point.