Home > Software engineering >  Why does eval consume twice the recursion limit?
Why does eval consume twice the recursion limit?

Time:09-15

I am aware of Why Python raises RecursionError before it exceeds the real recursion limit? question, but the answers there don't apply to following case:

from sys import getrecursionlimit
print(f'sys: maxRecursionDepth = {getrecursionlimit()}')
cnt = 0
def f3(s):
    global cnt
    cnt  = 1
    eval(s)
# ---
try:
    f3('f3(s)')
except RecursionError:
    print(f'f3() maxRecursionDepth =  {cnt}')

which outputs:

sys: maxRecursionDepth = 1000
f3() maxRecursionDepth =  333

It seems that calling eval() consumes two recursion levels before the counter in the recursive function is increased again.

I would like to know why it is that way? What is the reason for the observed behavior?

The answers in the link don't apply to the case above because they explain that the stack might be already used by other processes of Python and therefore the counter does not count up to the recursion limit. What is the difference between eval() and another functions which don't so massively fill the call stack? I would expect a 499 or similar as result, but not 333.

CodePudding user response:

The "recursion" limit doesn't actually care about recursion. It cares about how many stack frames you create. If you set it to something really low, you can trigger RecursionError without recursion.

>>> import sys
>>> sys.setrecursionlimit(4)
>>> def a():
...   b()
...
>>> def b():
...   print("Never here")
...
>>> a()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in a
  File "<stdin>", line 2, in b
RecursionError: maximum recursion depth exceeded while calling a Python object

In your case, it's the call to eval at each step that causes you to fill the call stack sooner than you expect.

CodePudding user response:

The eval calls is technically two stack levels:

  • The execution of eval itself.
  • The execution of eval's argument.

While eval is a builtin, the ()-call does not know that ahead of time – it still needs a stack level for itself. The argument is executed as a separate statement with the given or implicit globals/locals, and thus also needs a stack level.

The sneaky part is that eval prevents re-using the current call to f(3) to execute the statement for its next call. The added top-level is visible when not suppressing RecursionError:

...
  File "/Users/mistermiyagi/so_testbed.py", line 7, in f3
    eval(s)
  File "<string>", line 1, in <module>
  File "/Users/mistermiyagi/so_testbed.py", line 7, in f3
    eval(s)
  File "<string>", line 1, in <module>
...

Each File "<string>", line 1, in <module> is a separate frame for the top-level execution. The eval execution itself does not show up since it is a builtin call and thus has no Python frame, only a C stack level.

  • Related