Dabeaz

Dave Beazley's mondo computer blog. [ homepage | archive ]

Friday, May 27, 2011

 

Class decorators might also be super!

Recently Raymond Hettinger posted an amazing article Python's super() considered super!". Even if you think you know what super() does, you should go read it.

A commonly cited applications of super() is using it to implement a kind of cooperative inheritance as is sometimes found with mixin classes. Consider this code which is a slight variation of Raymond's example:

class LoggedSetItemMixin:
    def __setitem__(self,index,value):
        logging.info('Setting %r to %r', index,value)
        super().__setitem__(index,value)

Using this class, you could add logging to any class that implements __setitem__() by combining classes via multiple inheritance. For example:

class LoggingDict(LoggedSetItemMixin,dict):
    pass

class LoggingList(LoggedSetItemMixin,list):
    pass

Here's some sample output:

>>> d = LoggingDict()
>>> d['a'] = 1
INFO:root:Setting 'a' to 1
>>> e = LoggingList([0,1,2])
>>> e[0] = 99
INFO:root:Setting 0 to 99
>>>

The whole reason that this works is that super() delegates to the next class on the MRO. Thus, the __setitem__() call in LoggedSetItemMixin actually steps over to the next class in MRO of whatever kind of instance is being used. If you find this amazing, consider the fact that LoggedSetItemMixin is using super() even though it doesn't even specify a base class! It's pretty cool--maybe even a slight bit diabolical.

As amazing as this is, I've recently been thinking about a completely different approach to these kinds of problems based on class decorators. Consider this function:

def LoggedSetItem(cls):
    orig_setitem = cls.__setitem__
    def __setitem__(self, index, value):
        logging.info('Setting %r to %r' ,index, value)
        return orig_setitem(self,index,value)
    cls.__setitem__ = __setitem__
    return cls

This function is meant to be used as a decorator to class definitions. For example:

@LoggedSetItem
class LoggingDict(dict):
    pass

@LoggedSetItem
class LoggingList(list):
    pass

Carefully study the implementation of LoggedSetItem. As input, it receives a class object. It then looks up the unbound __setitem__ method and stores it in a variable. This lookup, as it turns out, is doing exactly the same work as super(). That is, it simply finds the implementation of the method being used by the class regardless of where it is actually located. After that, the function simply defines a replacement for __setitem__ with added logging and attaches it back to the class object. References to the original implementation of __setitem__ are held inside a closure so it all works out.

The class decorator approach has several notable features. First, it doesn't even involve the use of super() (or multiple inheritance for that matter). Second, as with super(), you don't have to hard-code any classnames--the class is simply passed in as an argument. Third,it has very good runtime performance. This is because the work normally performed by super() is only performed once, at the time of class decoration. Finally, there is a kind of built-in error checking. For example, if you try to apply the decorator to a class that doesn't support the required method, you will immediately get an error:

>>> @LoggedSetItem
class loggedint(int): pass

Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "logsetitem.py", line 9, in LoggedSetItem
    orig_setitem = cls.__setitem__
AttributeError: type object 'loggedint' has no attribute '__setitem__'
>>> 

As interesting as this is, I have no idea if using class decorators in this manner would be considered to be good practice or not. One potential problem is that by putting the code in a decorator, a lot of the work is performed just once at the time of class definition. If a program was playing sneaky tricks like dynamically changing method definitions at runtime, it clearly wouldn't work. There's also a certain risk that this approach is just too clever for it's own good.

Do you see any other downsides? I'd love to get your feedback.


Comments:
Class decorators have always struck me as how mixins should be written.
 
I actually think this is a perfect use for a decorator. It adds useful and general functionality in a way that's almost completely independent of the class definition.
 
It certainly seems conveniant and efficient, but I think that the use of decorators obscures the 'inheritance-like' effect that a use of super achieves. I suppose I would approve of them to add mixin-like enhancements (where more is needed than method decorators), but inheritance would be better than using them to monkeypatch existing classes (like super is used for).
 
That is _so_ much clearer than the hack based on super() order resolution.
 
A decorator defining a function inline and assigning itself into place "works" but I don't consider its implementation nearly as readable as a simple mixin class definition. Verdict: too clever for its own good.

One thing I _do_ like is the moving of work done per call via super() to being done at class definition time. That is presumably a performance win. If you're in a situation where calling through a class hierarchy is in your performance critical path perhaps this is a trick to consider. Granted you probably have a bigger design issue if you put something that complex in your critical path. ;)

I'm also wondering on the impact to testability of code using this approach:

If you for some reason need to prevent this class's overridden method from operating during a unittest (perhaps it is logging every __setitem__ call to twitter and you are losing followers) you can't simply stub out the method on the mixin class. Instead you must replace the method on _all_ classes that have used this decorator with one that does an appropriate super() call to direct it to the original method. Or you need to make sure your decorators are in their own library and be very careful to control when your module under test is imported so that it is only imported after you have replaced the decorator methods with test stub decorators that will not tweet.

Testing effects that happen at import time is doable, just more painful.
 
Post a Comment

Subscribe to Post Comments [Atom]





<< Home

Archives

Prior Posts by Topic

08/01/2009 - 09/01/2009   09/01/2009 - 10/01/2009   10/01/2009 - 11/01/2009   11/01/2009 - 12/01/2009   12/01/2009 - 01/01/2010   01/01/2010 - 02/01/2010   02/01/2010 - 03/01/2010   04/01/2010 - 05/01/2010   05/01/2010 - 06/01/2010   07/01/2010 - 08/01/2010   08/01/2010 - 09/01/2010   09/01/2010 - 10/01/2010   12/01/2010 - 01/01/2011   01/01/2011 - 02/01/2011   02/01/2011 - 03/01/2011   03/01/2011 - 04/01/2011   04/01/2011 - 05/01/2011   05/01/2011 - 06/01/2011   08/01/2011 - 09/01/2011   09/01/2011 - 10/01/2011   12/01/2011 - 01/01/2012   01/01/2012 - 02/01/2012   02/01/2012 - 03/01/2012  

This page is powered by Blogger. Isn't yours?

Subscribe to Posts [Atom]