Quantcast
Channel: Out Of What Box?
Viewing all articles
Browse latest Browse all 10

Python: Decorator Classes On The Edge

$
0
0

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.


Viewing all articles
Browse latest Browse all 10

Trending Articles