Calling super() from a method decorator

Using super(), you have to know the class to start from, but how to do that from a method decorator ? They get applied before the class can be referred to.
par Georges Racinet, mis à jour le 09/09/2012

(originally published on 2012-04-07)

How to systematically call super() from a decorator wrapping a function in Python 2 ? This  might prove useful in complex classes hierarchies, such as the ones that can be found in OpenERP context, where you'd want to relay something above, and you can't make assumptions on what actually lies there.

Before proceeding, let me state this : I have no clue what to say in a Python 3 context. All I know is that the super() syntax is different.

Suppose you have a simple decorator intended for methods with no return value:

>>> class A(object):
...     @mydecorator
...     def meth(self):
...         pass # put whatever you want

and imagine you'd want to call super() from the decorator. We find ourselves with a problem : on which class can we base this super ?

To illustrate, imagine we write this to systematically call super() after the decorated method:

>>> def mydecorator(func):
...      def wrapped(self, *args, **kwargs):
...             name = func.__name__
...             func(self, *args, **kwargs)
...             supermeth = getattr(super(self.__class__, self), name, None)
...             if supermeth is not None:
...                    supermeth(*args, **kwargs)
...      return wrapped
...

Then we get infinite loops for instances of subclasses (traceback shortened):

>>> class B(A):
...     pass
>>> b = B()
>>> b.meth()
RuntimeError: maximum recursion depth exceeded while calling a Python object

The problem is that self.__class__ is B if self is b, and therefore the super object amounts to B seen an instance of A, which calls again the meth() defined in A. More generally, it's a really bad idea not to really control the first argument of super().

Now, this is a bit tricky because in the decorator, we only have self and func(), we cannot directly grab A, which syntactically does not exist yet in the definition of the decorator. Indeed the declaration of A is roughly equivalent to the following:

>>> def meth(self): pass
>>> A = type('A', (object,), dict(meth=mydecorator(meth))

The only solution I could come with is to actually try and recognize the A class in self.__class__ inheritance hierarchy through the Model Resolution Order by checking that it's meth is the passed func. Actually, the passed func is not exactly the same as A.meth, as this example shows:

>>> def f(self): pass
>>> class T(object):
>>>      f = f
>>> T.f is f
False

But this is just because the point notation does the binding. On the other hand, the class __dict__ still holds the original:

>>> T.__dict__['f'] is f
True

Now putting it all together, the following works:

>>> def mydecorator(func):
...     def wrapped(self, *args, **kwargs):
...         name = func.__name__
... func(self, *args, **kwargs)
... for cls in self.__class__.__mro__: ... if cls.__dict__.get(name) is func: ... break ... supermeth = getattr(super(cls, self), name, None) ... if supermeth is not None: ... supermeth(*args, **kwargs) ... return wrapped

Well of course, in that case that's a lot of headache and possible performance overhead for, after all, some minor syntactical sugar, but it can be more useful than that.

Also, not to say that I'm proud of this solution :

  • Going through the whole MRO from the bottom up is definitely not elegant (easy to fix with a registry though)
  • A single method definition could be shared by two classes : after all, there's a reason why A.f is distinct from f
  • The robustness with respect to later monkey patching is an open question. I can imagine many scenarios.

In any case, keep in mind that a real-life solution to that problem would have of course to be more complex and tuned to its environment.