Python: Decorating with class through descriptors
As a fairly new Python developer, my first attempt at decorators hit a snag: my simple class-based decorator failed when decorating a method. I got around the immediate problem by rewriting the decorator as a function. Yet the episode left me wondering if there were some way to fix the class-based decorator to work when applied to methods. I’ve found what seems like an elegant solution, and picked up a better understanding of decorators and descriptors in the process.
Here’s an example that illustrates the original problem. DebugTrace is the decorator class:
class DebugTrace(object):
    def __init__(self, f):
        print("Tracing: {0}".format(f.__name__))
        self.f = f
    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
def greet():
    g = Greeter()
    g2 = Greeter()
    g.hello()
    g2.hello()
greet()
Running this with Python 2.6 or 3.1 results in an error:
Tracing: hello
Tracing: greet
Calling greet
Calling hello
Traceback (most recent call last):
  File "./DecoratorExample.py", line 31, in <module>
    greet()
  File "./DecoratorExample.py", line 8, in __call__
    return self.f(*args, **kwargs)
  File "./DecoratorExample.py", line 27, in greet
    g.hello()
  File "./DecoratorExample.py", line 8, in __call__
    return self.f(*args, **kwargs)
TypeError: hello() takes exactly 1 argument (0 given)
The output explains the problem. DebugTrace was instantiated only twice: once for Greeter.hello, and once for greet. That’s a reminder that decoration occurs during compile time, not run time. Accordingly, DebugTrace‘s reference to Greeter.hello represents an unbound function—it doesn’t reference any Greeter instances. So no ‘self’ argument was passed into the call to Greeter.hello; hence the TypeError.
All object-oriented languages that are worth knowing (and many that aren’t) allow container objects to redirect access to their contained objects. But of the languages that I’ve used, Python is unique in allowing object attributes to redirect calls that attempt to access them. Objects that implement this capability are called descriptors1. As we’ll soon see, function objects, which Python uses to implement methods, are descriptors.
- When the interpreter reads a class attribute, and the attribute value is an object that has a __get__method, then the return value of that__get__method is used as the attribute’s value.
- Methods are class attributes.
- Any callable object can serve as a method.
Here’s a good guide to descriptors. There’s also a recent post by Guido van Rossum, Python’s BDFL, that provides good background material on the feature.
To see where descriptors come into play, let’s look at the calling sequences for different versions of Greeter.hello. Here’s the rough sequence before Greeter.hello was decorated:
- The interpreter searches for an attribute named helloon theGreeterinstance, finding it in theGreeterclass object.
- The value of the Greeter.helloattribute (a function object) has a__get__method, making it a descriptor, so that__get__method is invoked. It’s passed a reference to theGreeterinstance (obj) through whichGreeter.hellowas called.
- The function object’s __get__method creates and returns another callable object, which we’ll refer to as a (bound) method object. The method object references both theGreeter.hellofunction andobj.
- The interpreter invokes the method object, passing it the arguments from the call to Greeter.hello(an empty list.) The method object then callsGreeter.hello, passingobjas the first argument, followed by the (empty) argument list.
When Greeter.hello is decorated with the DebugTracer class as shown above, a call to Greeter.hello runs more or less like this:
- The interpreter searches for an attribute named helloon theGreeterinstance, finding it in theGreeterclass object.
- The value of the Greeter.helloattribute is an instance ofDebugTrace. This isn’t a function object, and it doesn’t have a__get__method, but it does have a__call__method. That__call__method is invoked with the empty argument list.
- DebugTrace.__call__then calls- Greeter.hellowith the empty argument list.
- Since Greeter.hellowas looking for a single argument (self), rather than an empty argument list, aTypeErroris raised.
To fix DebugTrace, I turned it into a descriptor class, adding a __get__ method that fills the same role as a function object’s __get__ method. However, this method binds the Greeter instance to the callable DebugTrace object.
import types
# ...
    def __get__(self, obj, ownerClass=None):
        # Return a wrapper that binds self as a method of obj (!)
        return types.MethodType(self, obj)
Compare the new calling sequence for Greeter.hello to the sequence prior to decoration:
- The interpreter finds an attribute named helloin theGreeterclass object.
- The value of the Greeter.helloattribute is an instance ofDebugTrace, which is now a descriptor.DebugTrace.__get__is called, withobj(theGreeterinstance) passed as one of the arguments.
- DebugTrace.__get__creates and returns a method object. The method object references both the- DebugTraceinstance and- obj.
- The interpreter invokes the method object, passing it the arguments from the call to Greeter.hello(an empty list.) The method object then callsDebugTrace.__call__. That in turn callsGreeter.hello, passingobjas the first argument, followed by the (empty) argument list.
It’s worth noting that DebugTrace.__get__ is only invoked when accessing a DebugTrace object through an object’s class dictionary. Hence its presence has no effect on functions that aren’t methods, such as greet.
You can see the full, working example here (click on the “show source” link to view) :
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
def greet():
    g = Greeter()
    g2 = Greeter()
    g.hello()
    g2.hello()
greet()
Executing the new version gives the desired output:
Tracing: hello Tracing: greet Calling greet Calling hello *** Greeter 1 says hello! Calling hello *** Greeter 2 says hello!
__get__  method. For example, Ian Bicking wrote about a similar technique over a year and a half ago. However, Ian’s descriptor creates a new instance of the decorator class every time the method is invoked. I think the solution that I found—binding the original decorator instance to the method object—is different enough to be worth its own post.
For what it’s worth, I ran a simple performance test comparing Ian’s and my own class-based decorators, along with a function-based decorator. It showed no significant difference in performance among them. Apparently, the interpreter already optimizes these cases, which isn’t all that surprising.
1 I found the term “descriptor” to be somewhat confusing at first. A descriptor doesn’t really describe anything other than itself. To be fair, I don’t have any better suggestions. (“Redirector” ?) Naming is often one of the hardest challenges in software design, at least when it’s done right.
Python, Python Decorators, Python Descriptors
Just as I sat down to figure this out I spotted your post. Thanks!
Or you could just use functions to define decorators. Don’t forget how easy it is to use functools.wrap as well.
Don’t oop in the name of oop, that’s an oops.
Yes, you can use functions to define decorators (I even noted that in the first paragraph.) And that is often the appropriate path to take.
But my goal in this article (and its follow-up) wasn’t to show folks how to use a sledgehammer to kill a mosquito. (At least, not only that.) After I realized that I’d need to use descriptors to make a class-based decorator work, and figured out how to do so, I felt I’d reached a better understanding of both decorators and descriptors. My real goal was to present the two concepts in a way that would help others gain similar insights.