0

The question is: What is the difference between eval() and another functions which don't so massively fill the call stack? That eval() and exec() seem to differ from the other builtin functions and own functions in relation to stack space need is stated already in the question, so an answer that it is because it is that way is confirmation of the fact, but not an explanation why?

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 is it that way? What is the actual deep reason for the observed behavior?

The answers in the link above don't apply here 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. This does not explain: What is the difference between eval()/exec() and another functions which don't so massively fill the call stack? I would expect a 499 or similar as result, but not 333 out of 1000.

Claudio
  • 7,474
  • 3
  • 18
  • 48
  • But why do the answers there not apply to your case? – quamrana Sep 14 '22 at 16:40
  • 2
    Have you read the stack trace? It should have extra frames for `eval` itself f/e, showing you _exactly_ what's causing the overhead. – Charles Duffy Sep 14 '22 at 16:40
  • 4
    The "recursion" limit is really a limit on the depth of the call stack. You don't actually need recursion to reach the limit, if you define and call enough distinct functions. – chepner Sep 14 '22 at 16:41
  • what happens if you just print cnt, without calling eval at all? do you get 1000? – OrenIshShalom Sep 14 '22 at 16:41
  • 1
    yeah, because technically it is simply the stack depth, not really a recursion limit. – juanpa.arrivillaga Sep 14 '22 at 16:42
  • This is still curious. `eval()` should consume 1 level, so the result seems like it should be 500. – Barmar Sep 14 '22 at 16:44
  • I wonder if `eval` involves a call to `exec` that doesn't show up in the traceback (or something like that). – chepner Sep 14 '22 at 16:59
  • @CharlesDuffy : you ask *Have you read the stack trace?* . No I haven't for a quite simple reason: `I don't know how to read the stack trace` ... and I don't know what a 'stack trace' is. Sorry ... – Claudio Sep 15 '22 at 00:00
  • @OrenIshShalom you ask: *what happens if you just print cnt, without calling eval at all?* If I am not calling eval in the code above there will be no recursion and no printing. The counter will be then 1 if printed with an extra statement. By the way at 1000 limit the counter runs in simple case of recursion without eval up to 999. – Claudio Sep 15 '22 at 00:04
  • @quamrana : see my updated question for the answer to: *why do the answers there not apply to your case?* – Claudio Sep 15 '22 at 00:06
  • 2
    The stack trace is the error message that Python prints when it's telling you there's a RecursionError (or any other error). Everything below the `Traceback` line and above the exception itself is telling you the stack frames; those frames are what count against the recursion limit. It's not just about recursion; _every_ stack frame counts. – Charles Duffy Sep 15 '22 at 00:22
  • 1
    (there are languages that support "tail call optimization", where a function that calls another function at its end has its stack frame replaced with the other one instead of having the additional one appended to the stack; but Python is not one of those languages). – Charles Duffy Sep 15 '22 at 00:23

2 Answers2

3

Each eval call needs 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 cannot be "inlined" and still needs a stack level to execute. 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 f3(…) 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.


Let's look at a simpler example without the complications of counting recursions:

def direct():
    direct()

def via_eval():
    eval("via_eval()")

We can call each of those directly; the stack trace then reveals the Python frames involved.

  • The direct call shows just the direct frames. The stack thus looks like this:

    … -> direct() -> direct() -> …
    
  • The via_eval call also shows the File "<string>", line 1, in <module> frames; while eval has no frame we know it is called. The stack thus looks like this:

    … -> via_eval() -> eval() -> <module> -> via_eval() -> eval() -> <module> …
    

This is why the stack level – and thus the speed at which we hit recursion limit – increases by factor 3 instead of 2.

To understand why eval() needs a separate <module>, we can look at a related concept: eval is to calls what functions are to code.

# equivalent to def eval(code): …
def call(func):
    func()

def via_call():
    # equivalent to eval("via_call()")
    call(lambda: via_call())

This is a 1:1 translation of the via_eval setup. The traceback1 equally shows that the helper that performs the execution is separate from the helper being executed.

  … -> via_call() -> call() -> <lambda> -> via_call() -> call() -> <lambda> …

Just like call here triggers the execution of something else (a function) eval also triggers the execution of something else (a code object). Since the trigger (call/eval) and the triggered (lambda: via_call())/'via_eval()') are separate things each needs its own stack level.


1

  File "/Users/mistermiyagi/so_testbed.py", line 6, in via_call
    call(lambda: via_call())
  File "/Users/mistermiyagi/so_testbed.py", line 2, in call
    func()
  File "/Users/mistermiyagi/so_testbed.py", line 6, in <lambda>
    call(lambda: via_call())
MisterMiyagi
  • 44,374
  • 10
  • 104
  • 119
  • Hmmm ... `print(exec(str(eval(s))))` instead of `eval(s)` should add two another frames on the stack for each additional builtin function call further reducing the recursion depth, right? – Claudio Sep 14 '22 at 18:23
  • ... and also `print(f3(exec(eval(str(s)))))` instead of `eval(s)` should reduce the counted recursion depth (all are builtin functions) ... but ... it doesn't. Hmmm ... – Claudio Sep 14 '22 at 18:31
  • @Claudio Since `f3` never returns, `eval` never returns either. So in `print(f3(exec(eval(str(s)))))` only the inner `eval(str(s))` executes – the outer `f3`, and `exec` wait for it to finish since they need its *result* as an argument. – MisterMiyagi Sep 14 '22 at 18:46
  • There are interesting answers to the issue of nesting of function calls and why they don't contribute to the stack remaining without impact on recursion depth here: https://stackoverflow.com/questions/73723129/python-nested-vs-chained-function-calls-in-recursion (Python nested vs chained function calls in recursion) down to the statement *You only add to the recursion depth when the body of a function calls another function.* – Claudio Sep 15 '22 at 00:12
  • Are eval() and exec() the only builtin function which require *two* stack levels? Sholdn't `print(f'{f3()}') also need *two* stack levels as first the f-string has to be evaluated what needs kind of eval() for the string content? It's still not cleas *what is so special* about eval() and exec()? – Claudio Sep 15 '22 at 01:11
  • @Claudio An f-string is evaluated in the current frame – that is its entire point. `f'{f3()}'` is functionally equivalent to just `f3()` and `f'Foo {f3()} Bar'` to just `'Foo ' + str(f3()) + ' Bar'`. I am afraid it is difficult to explain to you what is special when it is not clear what *normal* parts you are familiar with. – MisterMiyagi Sep 15 '22 at 06:42
  • In other words part of the parsing work for an f-string is done at the level of compilation to byte-code, so no function call is required? But print() is like eval() a function, doesn't it? And needs its parameter to be evaluated. So what is it that makes such big difference between eval and print, str? Will map() behave more like eval? (I haven't checked it out yet) – Claudio Sep 15 '22 at 06:55
  • @Claudio The argument to `print` is `f3()` – the *result* of the call – but the argument to `eval` is `'f3()'` – the *instruction* to call. `eval` is required to execute `f3()` by itself. – MisterMiyagi Sep 15 '22 at 07:04
  • 1
    @Claudio In the end, it's not really worth worrying about. Recursion should be avoided when possible in Python both because of function-call overhead and the stack limit; how *quickly* the stack runs out shouldn't be something you concern yourself with. (Basically, reserve recursion for times when the recursion tree's height is logarithmic in the number of recursive calls--tree recursion--rather than linear.) – chepner Sep 15 '22 at 11:34
2

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.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • This still does not explain how it comes that eval consumes so much of the call stack? 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*. – Claudio Sep 14 '22 at 16:58
  • My guess is that `eval` does two things (or something similar): uses `compile` to generate a code object from its argument, then executes that code object with `exec`. I don't see anything that resembles `exec` in the stack as shown by the traceback, but I'm not sure if the frames in the traceback are necessarily an exact representation of the call stack. – chepner Sep 14 '22 at 17:01
  • `exec` seem to behave same way as `eval` consuming two recursion steps, so if `eval` would call `exec` four and not only two frames would be put on stack. There must be another reason why two frames are necessary for eval and exec, but not for example print() using f-string, right? – Claudio Sep 15 '22 at 01:38