A fascinating behavior, I did not get to the root of the issue but I still would like to share some logging code that I wrote that might help others find the issue:
m = {0:0, 1:1}
def f(n, depth=0):
print(f"{' '*depth}f({n})")
if n not in m:
print(f"{' '*depth}Computing f({n-1}) + f({n-2})")
m[n] = sum(f(n - i, depth+1) for i in [1, 2])
else:
print(f"{' '*depth}Already computed f({n})")
return m[n]
print("Testing the version with the iterator, call stack is:")
f(10)
m = {0:0, 1:1}
def g(n, depth=0):
print(f"{' '*depth}g({n})")
if n not in m:
print(f"{' '*depth}Computing g({n-1}) + g({n-2})")
m[n] = g(n - 1, depth+1) + g(n - 2, depth+1)
else:
print(f"{' '*depth}Already computed g({n})")
return m[n]
print("Testing the version with the normal sum, call stack is:")
g(10)
The output is:
Testing the version with the iterator, call stack is:
f(10)
Computing f(9) + f(8)
f(9)
Computing f(8) + f(7)
f(8)
Computing f(7) + f(6)
f(7)
Computing f(6) + f(5)
f(6)
Computing f(5) + f(4)
f(5)
Computing f(4) + f(3)
f(4)
Computing f(3) + f(2)
f(3)
Computing f(2) + f(1)
f(2)
Computing f(1) + f(0)
f(1)
Already computed f(1)
f(0)
Already computed f(0)
f(1)
Already computed f(1)
f(2)
Already computed f(2)
f(3)
Already computed f(3)
f(4)
Already computed f(4)
f(5)
Already computed f(5)
f(6)
Already computed f(6)
f(7)
Already computed f(7)
f(8)
Already computed f(8)
Testing the version with the normal sum, call stack is:
g(10)
Computing g(9) + g(8)
g(9)
Computing g(8) + g(7)
g(8)
Computing g(7) + g(6)
g(7)
Computing g(6) + g(5)
g(6)
Computing g(5) + g(4)
g(5)
Computing g(4) + g(3)
g(4)
Computing g(3) + g(2)
g(3)
Computing g(2) + g(1)
g(2)
Computing g(1) + g(0)
g(1)
Already computed g(1)
g(0)
Already computed g(0)
g(1)
Already computed g(1)
g(2)
Already computed g(2)
g(3)
Already computed g(3)
g(4)
Already computed g(4)
g(5)
Already computed g(5)
g(6)
Already computed g(6)
g(7)
Already computed g(7)
g(8)
Already computed g(8)
So they look to be doing the exact same thing... my Python version is 3.8.15, You should try running this code to see if the output is the same also with your Python version.
On my Python version, I hit the recursion limit for f(333)
and g(997)
Edit: thanks to John Kugelman I am getting closer to the answer, with the code:
import time
import inspect
m = {0:0, 1:1}
def f(n, depth=0):
# fraction part of time
print(f"{' '*depth}f({n})")
if n not in m:
print(f"{' '*depth}Computing f({n-1}) + f({n-2})")
m[n] = sum(f(n - i, depth+1) for i in [1, 2])
else:
print(f"{' '*depth}Already computed f({n})")
print(f"{' '*depth}Returning {m[n]}")
print(f"{' '*depth}Call stack has length {len(inspect.stack())} and is: {[x.function for x in inspect.stack()]}")
return m[n]
print("Testing the version with the iterator, call stack is:")
start = time.time()
f(10)
m = {0:0, 1:1}
def g(n, depth=0):
print(f"{' '*depth}g({n})")
if n not in m:
print(f"{' '*depth}Computing g({n-1}) + g({n-2})")
m[n] = g(n - 1, depth+1) + g(n - 2, depth+1)
else:
print(f"{' '*depth}Already computed g({n})")
print(f"{' '*depth}Returning {m[n]}")
print(f"{' '*depth}Call stack has length {len(inspect.stack())} and is: {[x.function for x in inspect.stack()]}")
return m[n]
print("Testing the version with the normal sum, call stack is:")
g(10)
import sys
print(sys.version)
The output is:
Testing the version with the iterator, call stack is:
f(10)
Computing f(9) + f(8)
f(9)
Computing f(8) + f(7)
f(8)
Computing f(7) + f(6)
f(7)
Computing f(6) + f(5)
f(6)
Computing f(5) + f(4)
f(5)
Computing f(4) + f(3)
f(4)
Computing f(3) + f(2)
f(3)
Computing f(2) + f(1)
f(2)
Computing f(1) + f(0)
f(1)
Already computed f(1)
Returning 1
Call stack has length 20 and is: ['f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<module>']
f(0)
Already computed f(0)
Returning 0
Call stack has length 20 and is: ['f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<module>']
Returning 1
Call stack has length 18 and is: ['f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<module>']
f(1)
Already computed f(1)
Returning 1
Call stack has length 18 and is: ['f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<module>']
Returning 2
Call stack has length 16 and is: ['f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<module>']
f(2)
Already computed f(2)
Returning 1
Call stack has length 16 and is: ['f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<module>']
Returning 3
Call stack has length 14 and is: ['f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<module>']
f(3)
Already computed f(3)
Returning 2
Call stack has length 14 and is: ['f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<module>']
Returning 5
Call stack has length 12 and is: ['f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<module>']
f(4)
Already computed f(4)
Returning 3
Call stack has length 12 and is: ['f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<module>']
Returning 8
Call stack has length 10 and is: ['f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<module>']
f(5)
Already computed f(5)
Returning 5
Call stack has length 10 and is: ['f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<module>']
Returning 13
Call stack has length 8 and is: ['f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<module>']
f(6)
Already computed f(6)
Returning 8
Call stack has length 8 and is: ['f', '<genexpr>', 'f', '<genexpr>', 'f', '<genexpr>', 'f', '<module>']
Returning 21
Call stack has length 6 and is: ['f', '<genexpr>', 'f', '<genexpr>', 'f', '<module>']
f(7)
Already computed f(7)
Returning 13
Call stack has length 6 and is: ['f', '<genexpr>', 'f', '<genexpr>', 'f', '<module>']
Returning 34
Call stack has length 4 and is: ['f', '<genexpr>', 'f', '<module>']
f(8)
Already computed f(8)
Returning 21
Call stack has length 4 and is: ['f', '<genexpr>', 'f', '<module>']
Returning 55
Call stack has length 2 and is: ['f', '<module>']
Testing the version with the normal sum, call stack is:
g(10)
Computing g(9) + g(8)
g(9)
Computing g(8) + g(7)
g(8)
Computing g(7) + g(6)
g(7)
Computing g(6) + g(5)
g(6)
Computing g(5) + g(4)
g(5)
Computing g(4) + g(3)
g(4)
Computing g(3) + g(2)
g(3)
Computing g(2) + g(1)
g(2)
Computing g(1) + g(0)
g(1)
Already computed g(1)
Returning 1
Call stack has length 11 and is: ['g', 'g', 'g', 'g', 'g', 'g', 'g', 'g', 'g', 'g', '<module>']
g(0)
Already computed g(0)
Returning 0
Call stack has length 11 and is: ['g', 'g', 'g', 'g', 'g', 'g', 'g', 'g', 'g', 'g', '<module>']
Returning 1
Call stack has length 10 and is: ['g', 'g', 'g', 'g', 'g', 'g', 'g', 'g', 'g', '<module>']
g(1)
Already computed g(1)
Returning 1
Call stack has length 10 and is: ['g', 'g', 'g', 'g', 'g', 'g', 'g', 'g', 'g', '<module>']
Returning 2
Call stack has length 9 and is: ['g', 'g', 'g', 'g', 'g', 'g', 'g', 'g', '<module>']
g(2)
Already computed g(2)
Returning 1
Call stack has length 9 and is: ['g', 'g', 'g', 'g', 'g', 'g', 'g', 'g', '<module>']
Returning 3
Call stack has length 8 and is: ['g', 'g', 'g', 'g', 'g', 'g', 'g', '<module>']
g(3)
Already computed g(3)
Returning 2
Call stack has length 8 and is: ['g', 'g', 'g', 'g', 'g', 'g', 'g', '<module>']
Returning 5
Call stack has length 7 and is: ['g', 'g', 'g', 'g', 'g', 'g', '<module>']
g(4)
Already computed g(4)
Returning 3
Call stack has length 7 and is: ['g', 'g', 'g', 'g', 'g', 'g', '<module>']
Returning 8
Call stack has length 6 and is: ['g', 'g', 'g', 'g', 'g', '<module>']
g(5)
Already computed g(5)
Returning 5
Call stack has length 6 and is: ['g', 'g', 'g', 'g', 'g', '<module>']
Returning 13
Call stack has length 5 and is: ['g', 'g', 'g', 'g', '<module>']
g(6)
Already computed g(6)
Returning 8
Call stack has length 5 and is: ['g', 'g', 'g', 'g', '<module>']
Returning 21
Call stack has length 4 and is: ['g', 'g', 'g', '<module>']
g(7)
Already computed g(7)
Returning 13
Call stack has length 4 and is: ['g', 'g', 'g', '<module>']
Returning 34
Call stack has length 3 and is: ['g', 'g', '<module>']
g(8)
Already computed g(8)
Returning 21
Call stack has length 3 and is: ['g', 'g', '<module>']
Returning 55
Call stack has length 2 and is: ['g', '<module>']
3.8.15 (default, Nov 24 2022, 15:19:38)
[GCC 11.2.0]
So the problem is that the generator expression is getting put on the call stack, taking up space that could be used by the actual recursive function. Now it would be interesting to investigate why the generator expression is getting put on the stack as if it were a function.