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)
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.
Python, Python Decorators, Python Descriptors