8

I've been playing around with memoization and recursion in python 3.3

Ignoring the fact that python is the wrong language to be doing this in, I've found that I get inconsistent results between using functools.lru_cache to memoize, and not using functools.lru_cache

I'm not changing the recursion limit - it stays at the default, which for me is 1000.

To test the problem, I've written up a simple recursive function to sum numbers from 1 through i

#!/usr/bin/python

def sumtil(i):
"""Recursive function to sum all numbers from 1 through i"""

    # Base case, the sum of all numbers from 1 through 1 is 1... 
    if i == 1:
        return 1
    else:
        return i+sumtil(i-1)

# This will not throw an exception
sumtil(998)

# This will throw an exception
sumtil(999)

Running this function normally, I can run sumtil(998) comfortably without hitting the recursion limit. sumtil(999) or above will throw an exception.

However, if I try decorating this function with @functools.lru_cache(), the recursion limit exception is thrown 3 times earlier, when running sumtil(333)

#!/usr/bin/python

import functools 

@functools.lru_cache(maxsize=128)
def sumtil(i):
    """Recursive function to sum all numbers from 1 through i"""

    # Base case, the sum of all numbers from 1 through 1 is 1... 
    if i == 1:
        return 1
    else:
        return i+sumtil(i-1)

# This will not throw an exception
sumtil(332)

# This will throw an exception
sumtil(333)

Being that 332*3 = 996, but 333*3 = 999, it appears to me that the lru_cache decorator is causing each level of recursion in my function to become three levels of recursion.

Why do I get three times as many levels of recursion when using functools.lru_cache to memoize a function?

Kiirani
  • 1,067
  • 7
  • 19

1 Answers1

7

Because a decorator is an extra function, so it "uses" one level in the stack. Example:

>>> def foo(f):
...   def bar(i):
...     if i == 1:
...       raise Exception()
...     return f(i)
...   return bar
...
>>> @foo
... def sumtil(i):
...     if i == 1:
...         return 1
...     else:
...         return i+sumtil(i-1)
...
>>> sumtil(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in bar
  File "<stdin>", line 6, in sumtil
  File "<stdin>", line 5, in bar
  File "<stdin>", line 6, in sumtil
  File "<stdin>", line 4, in bar
Exception
>>>

Besides, if the decorator uses argument packing/unpacking, then an extra level is used (though I'm not knowledgeable enough about the Python runtime to explain why that happens).

def foo(f):
  def bar(*args,**kwargs):
    return f(*args,**kwargs)
  return bar

Max. recursion depth exceeded:

  • undecorated: 1000
  • w/o packing: 500
  • with packing: 334
mgibsonbr
  • 21,755
  • 7
  • 70
  • 112