Python: Decorator Classes On The Edge

OK, I cheated.

In yesterday’s post on writing decorator classes that decorate methods, I left out two edge cases that can’t be completely ignored: static methods and class methods.

To illustrate, I’ll start where I left off yesterday, adding a decorated class method and a decorated static method to the example:

import types

class DebugTrace(object):
    def __init__(self, f):
        print("Tracing: {0}".format(f.__name__))
        self.f = f

    def __get__(self, obj, ownerClass=None):
        # Return a wrapper that binds self as a method of obj (!)
        return types.MethodType(self, obj)

    def __call__(self, *args, **kwargs):
        print("Calling: {0}".format(self.f.__name__))
        return self.f(*args, **kwargs)


class Greeter(object):
    instances = 0

    def __init__(self):
        Greeter.instances += 1
        self._inst = Greeter.instances

    @DebugTrace
    def hello(self):
        print("*** Greeter {0} says hello!".format(self._inst))

    @DebugTrace
    @classmethod
    def classHello(cls, to):
        print("*** The {0} class says hello to {1}".format(cls.__name__, to))

    @DebugTrace
    @staticmethod
    def staticHello(to):
        print("*** Something says hello to " + to)


@DebugTrace
def greet():
    g = Greeter()
    g2 = Greeter()
    g.hello()
    g2.hello()
    Greeter.staticHello("you")
    Greeter.classHello("everyone")

greet()

Running this gives an error:


Tracing: hello
Traceback (most recent call last):
  File "DecoratorExample.py", line 17, in <module>
    class Greeter(object):
  File "DecoratorExample.py", line 29, in Greeter
    @classmethod
  File "DecoratorExample.py", line 5, in __init__
    print("Tracing: {0}".format(f.__name__))
AttributeError: 'classmethod' object has no attribute '__name__'

Just for this example, I’ll try removing the “Tracing” print call; but still no joy:

Calling: greet
Calling: hello
*** Greeter 1 says hello!
Calling: hello
*** Greeter 2 says hello!
Traceback (most recent call last):
  File "DecoratorExample.py", line 48, in <module>
    greet()
  File "DecoratorExample.py", line 14, in __call__
    return self.f(*args, **kwargs)
  File "DecoratorExample.py", line 45, in greet
    Greeter.staticHello("you")
  File "DecoratorExample.py", line 10, in __get__
    return types.MethodType(self, obj)
TypeError: self must not be None

The essential problem is that class methods and static methods are not callable.1 There’s an easy enough workaround: always use @staticmethod or @classmethod as the outermost (i.e., last) decorator in a sequence, as in:

    @classmethod
    @DebugTrace
    def classHello(cls, to):
        print("*** The Greeter class says hello to " + to)

    @staticmethod
    @DebugTrace
    def staticHello(to):
        print("*** Something says hello to " + to)

That produces the desired result:

Tracing: hello
Tracing: classHello
Tracing: staticHello
Tracing: greet
Calling: greet
Calling: hello
*** Greeter 1 says hello!
Calling: hello
*** Greeter 2 says hello!
Calling: staticHello
*** Something says hello to you
Calling: classHello
*** The Greeter class says hello to everyone

But suppose we really, really need to decorate an already-decorated classmethod or staticmethod. The key lies again in the descriptor protocol.

First, we need to modify the decorator’s __init__ method. (Note that the only reason that we need to modify __init__ is to find the name of the classmethod or staticmethod that’s being decorated. If we didn’t produce the “Tracing:” output, we could leave __init__ alone.)

The new __init__ method detects whether the passed “function” has a __call__ method. If it doesn’t, then it’s reasonable to assume that it’s a classmethod or a staticmethod. Calling the object’s __get__ method returns a function object, from which we can get the function name:

    def __init__(self, f):
        self.f = f
        if hasattr(f, "__call__"):
            name = self.f.__name__
        else:
            # f is a class or static method.
            tmp = f.__get__(None, f.__class__)
            name = tmp.__name__
        print("Tracing: {0}".format(name))

In the decorator’s __get__ method, we’ll know that we’re dealing with a staticmethod or classmethod if the passed obj has the value None. If that’s the case, then we make a one-time adjustment to self.f, ensuring that it points to the underlying function.

Wait—why didn’t we do this in DebugTrace.__init__? It may seem redundant, but the call to f.__get__ that we made in DebugTrace.__init__ doesn’t count: that call didn’t specify the class that f actually belongs to. (Any class works for the purpose of getting the function’s name.) Now that we’re in DebugTrace.__get__, we know via the ownerClass parameter the class that self.f is associated with. This class may make its way into a classmethod call (e.g., the call to Greeter.classHello), so it matters that we get it right.

Note that we return self in this case. We don’t want to create a new method object for classmethods or staticmethods; just calling self.__call__ will call the method appropriately.

    def __get__(self, obj, ownerClass=None):
        if obj is None:
            f = self.f
            if not hasattr(f, "__call__"):
                self.f = f.__get__(None, ownerClass)
            return self
        else:
            # Return a wrapper that binds self as a method of obj (!)
            return types.MethodType(self, obj)
Setting self.f as above might raise thread-safety issues, especially if you don’t want to rely on the atomicity of modifying a dict in-place. Borrowing from Ian Bicking’s solution, which returns a copy of the decorator for each call to __get__, can help us dodge the concurrency bullet. We’d replace

            return self

with

            return self.__class__(self.f)

However, this results in any side effects in the decorator’s __init__ method being re-executed for every call to the decorated method. Note the additional “Tracing:” lines in the output here:

Tracing: hello
Tracing: classHello
Tracing: staticHello
Tracing: greet
Calling: greet
Calling: hello
*** Greeter 1 says hello!
Calling: hello
*** Greeter 2 says hello!
Tracing: staticHello
Calling: staticHello
*** Something says hello to you
Tracing: classHello
Calling: classHello
*** The Greeter class says hello to everyone

Another option, of course, is to use a mutex around the statement that modifies self.f.

The decorator’s __call__ method is unchanged from yesterday’s example. As before, it simply prints out the desired trace message, then invokes self.f.

Here’s the entire decorator, as revised:

import types

class DebugTrace(object):
    def __init__(self, f):
        self.f = f
        if hasattr(f, "__call__"):
            name = self.f.__name__
        else:
            # f is a class or static method
            tmp = f.__get__(None, f.__class__)
            name = tmp.__name__
        print("Tracing: {0}".format(name))

    def __get__(self, obj, ownerClass=None):
        if obj is None:
            f = self.f
            if not hasattr(f, "__call__"):
                self.f = f.__get__(None, ownerClass)
            return self
        else:
            # Return a wrapper that binds self as a method of obj (!)
            return types.MethodType(self, obj)

    def __call__(self, *args, **kwargs):
        print("Calling: {0}".format(self.f.__name__))
        return self.f(*args, **kwargs)


class Greeter(object):
    instances = 0

    def __init__(self):
        Greeter.instances += 1
        self._inst = Greeter.instances

    @DebugTrace
    def hello(self):
        print("*** Greeter {0} says hello!".format(self._inst))

    @DebugTrace
    @classmethod
    def classHello(cls, to):
        print("*** The {0} class says hello to {1}".format(cls.__name__, to))

    @DebugTrace
    @staticmethod
    def staticHello(to):
        print("*** Something says hello to " + to)


@DebugTrace
def greet():
    g = Greeter()
    g2 = Greeter()
    g.hello()
    g2.hello()
    Greeter.staticHello("you")
    Greeter.classHello("everyone")

greet()

I’ve tested this with Python 2.6, 2.7, and 3.1.


1 Without taking a deep dive into Python’s history, I couldn’t say why they’re not callable. But it does seem that class methods and static methods were never intended to be used frequently.

, ,

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>