recursive decorator

Ethan Furman ethan at stoneleaf.us
Thu Sep 3 12:41:05 EDT 2009


Michele Simionato wrote:
> On Sep 3, 12:19 am, Ethan Furman <et... at stoneleaf.us> wrote:
> 
>>Greetings, List!
>>
>>The recent thread about a recursive function in a class definition led
>>me back to a post about bindfunc from Arnaud, and from there I found
>>Michele Simionato's decorator module (many thanks! :-), and from there I
>>began to wonder...
>>
>>from decorator import decorator
>>
>>@decorator
>>def recursive1(func, *args, **kwargs):
>>     return func(func, *args, **kwargs)
>>
>>@recursive1
>>def factorial1(recurse, n):
>>     if n < 2:
>>         return 1
>>     return n * recurse(n-1)
>>
>>factorial(4)
>>TypeError: factorial1() takes exactly 2 arguments (1 given)
> 
> 
> What are you trying to do here? I miss why you don't use the usual
> definition of factorial.
> If you have a real life use case which is giving you trouble please
> share. I do not see
> why you want to pass a function to itself (?)
> 
>                             M. Simionato

Factorial is an example only.

The original thread by Bearophile:
   http://mail.python.org/pipermail/python-list/2009-May/711848.html

A similar thread from kj (where scopes is the actual problem):
   http://mail.python.org/pipermail/python-list/2009-August/725174.html

Basic summary:  the recursive decorator (the one you snipped, not the 
one showing above), is a replacement for the bindfunc decorator, and 
preserves the signature of the decorated function, minus the 
self-referring first parameter.  It solves kj's problem (although there 
are arguably better ways to do so), and I think helps with Bearophiles 
as well.  As a bonus, the tracebacks are more accurate if the function 
is called incorrectly.  Consider:

In [1]: def recursive(func):
    ...:     def wrapper(*args, **kwargs):
    ...:         return func(wrapper, *args, **kwargs)
    ...:     return wrapper
    ...:

In [2]: @recursive
    ...: def factorial(recurse, n):
    ...:     if n < 2:
    ...:         return 1
    ...:     else:
    ...:         return n * recurse(n-1)
    ...:

In [3]: factorial(1, 2)  # in incorrect call
--------------------------------------------------------------------
TypeError                          Traceback (most recent call last)

C:\pythonlib\<ipython console> in <module>()

C:\pythonlib\<ipython console> in wrapper(*args, **kwargs)

TypeError: factorial() takes exactly 2 arguments (3 given)
                                      ^^^^^^^^^^^^^^^^^^^^^
                    mildly confusing /

----------------------------------------
versus the error with the new decorator:
----------------------------------------

In [2]: factorial(4, 5)  # another incorrect call
--------------------------------------------------------------------
TypeError                          Traceback (most recent call last)

C:\pythonlib\<ipython console> in <module>()

TypeError: factorial() takes exactly 1 argument (2 given)
                                      ^^^^^^^^^^^^^^^^^^^^

This latter error matches how factorial actually should be called, both 
in normal code using it, and in the function itself, calling itself.

So that's the why and wherefore.  Any comments on the new decorator 
itself?  Here it is again:

def recursive(func):
     """
     recursive is a signature modifying decorator.
     Specifially, it removes the first argument, so
     that calls to the decorated function act as if
     it does not exist.
     """
     argspec = inspect.getargspec(func)
     first_arg = argspec[0][0]
     newargspec = (argspec[0][1:], ) + argspec[1:]
     fi = decorator.FunctionMaker(func)
     old_sig = inspect.formatargspec( \
         formatvalue=lambda val: "", *argspec)[1:-1]
     new_sig = inspect.formatargspec( \
         formatvalue=lambda val: "", *newargspec)[1:-1]
     new_def = '%s(%s)' % (fi.name, new_sig)
     body = 'return func(%s)' % old_sig
     def wrapper(*newargspec):
         return func(wrapper, *newargspec)
     evaldict = {'func':func, first_arg:wrapper}
     return fi.create(new_def, body, evaldict, \
          doc=fi.doc, module=fi.module)

~Ethan~



More information about the Python-list mailing list