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