Welcome, guest | Sign In | My Account | Store | Cart

Recipe/Tools for easy and stable recursion of current function when only selected arguments have to be changed. No more mixup of argument positions/names. No more forgetting. Useful as well for realizing delayed/repeated/serialized function calls.

Python, 117 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# simple dirty trick for automatic recursion: keyword args from locals()

def myfunc(a,b=1,c=2,d=3,e=4):
    _kwargs=locals().copy()    # freeze unconditional at very beginning
    print locals()
    if e<=0: return
    else:
        _kwargs['e']=e-1
        myfunc(**_kwargs)

# proper tools for correct/ad-hoc/fast automatic recursion

def recursion_args(func, func_locals, **kwargs):
    """returns named positional args only - without original *args, **kwargs"""
    co=func.func_code
    return tuple([ kwargs.pop(co.co_varnames[i], func_locals[co.co_varnames[i]])
                   for i in range(co.co_argcount) ])
    assert not kwargs

def recurse(func, func_locals, *args, **kwargs):
    """for ad-hoc recursion - in case: manual insertion of original *args/**kwargs.
    Use it like  recurse(myfunc,locals(),changed=changed-1,*args,**kwargs)"""
    co=func.func_code
    args = tuple([ kwargs.pop(co.co_varnames[i], func_locals[co.co_varnames[i]])
                   for i in range(co.co_argcount) ]) + args
    return func(*args,**kwargs)

def recurse_ex(func, func_locals, **kwargs):
    """auto-handles original *args and **kwargs.
    Use it like recurse_ex(myfunc,locals(),changed=changed-1)
    """
    co=func.func_code
    args = tuple([ kwargs.pop(co.co_varnames[i], func_locals[co.co_varnames[i]])
                   for i in range(co.co_argcount) ])
    if co.co_flags & 0x04:
        i+=1
        args+=func_locals[co.co_varnames[i]]
    if co.co_flags & 0x08:
        kwargs.update(func_locals[co.co_varnames[i+1]]) #TODO:reverse priority?
    return func(*args,**kwargs)

def recursor(func, func_locals):
    co=func.func_code
    def _recursor(**kwargs):
        args=tuple([kwargs.pop(co.co_varnames[i],func_locals[co.co_varnames[i]])
                    for i in range(co.co_argcount) ])
        return func(*args,**kwargs)
    return _recursor

def recursor1(func):
    # for recursion with some pre-computatation; 
    co=func.func_code
    argnames=[ co.co_varnames[i] for i in range(co.co_argcount) ]
    def _recursor1(func_locals,**kwargs):
        return func( *tuple([ kwargs.pop(name,func_locals[name])
                              for name in argnames ]), **kwargs )
    return _recursor1

# examples

def func1(a,b=1,c=2,d=3,e=4):
    print locals()
    if e<=0: return
    else:
        recurse(func1,locals(),
                e=e-1 )               # recurse with e changed

def func2(a,b=1,c=2,d=3,e=4):
    RECURSE=recursor(func2,locals())  # freeze it at the beginning
    print locals()
    if e<=0: return
    else:
        RECURSE(e=e-1)     

def func3(a,b=1,c=2,d=3,e=4):
    print locals()
    if e<=0: return
    else:
        func3_recurse(locals(),
                      e=e-1 )
func3_recurse=recursor1(func3)

def func4(a,b=5,*args,**kwargs):
    _v=1
    print locals()
    if b<=0: return
    else:
        recurse_ex(func4,locals(),
                   b=b-1 )

echo_args=None
def func_with_echo(a,b=1,c=2,d=3,e=4, echo=None):
    print locals()
    global echo_args
    if not echo:
        if echo_args:
            func_with_echo(*echo_args)
        echo_args=recursion_args(func_with_echo, locals(),
                                 echo=1)

class Class:
    def meth(self,a,b=1,c=2):
        print locals()
        if c<=0: return
        else:
            recurse(Class.meth,locals(),
                    c=c-1)

if __name__=='__main__':
    myfunc('myfunc')
    func1('func-1')
    func2('func-2')
    func3('func-3')
    func4('func-4',5,4,3,2,1,extra='e')
    Class().meth('E')
    func_with_echo('first')
    func_with_echo('second',c=7)

Generally useful. Often used in error recover/retry strategies.

Tested with Python 2.3, 2.4, 2.5. For Python2.2 the .pop()'s have to be replaced with .get() / worked around.

Maybe the recursor could be considered for the new 'functional' builtin module of Python 2.5 - and better reflexion options built-in for a future Python.

Speed impact on an elementary simple 2-liner recursion (Py2.3; print's removed): profile.run("for i in range(1000):func0('A')") : 81ms profile.run("for i in range(1000):func1('A')") : 285ms (recurse) profile.run("for i in range(1000):func2('A')") : 446ms (recursor) profile.run("for i in range(1000):func3('A')") : 239ms (recursor1) def func0(a,b=1,c=2,d=3,e=4): ____if e<=0: return ____else: func0(a,b,c,d,e-1)

Created by R K on Sat, 15 Apr 2006 (PSF)
Python recipes (4591)
R K's recipes (5)

Required Modules

  • (none specified)

Other Information and Tasks