3 min read

Calling super() from a method decorator

Calling super() from a method decorator

(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.