Interesting and unfortunate. Is this still the case in 2024?
Interesting and unfortunate. Is this still the case in 2024?
def f(x):
if x > 0:
return f(x-1)
return 0
# Here is where a compiler might assume that f(x) is tail recursive, and so do TCE
g = f
def f(x):
return x
g(5)
So according to the python standard, g(5) returns 4, since it calls the original f, but then the first recursive call will go to the new f. If f was TCE'd, then it would return 0, as the actual recursion would be eliminated, so it wouldn't matter that you've reassigned f to a new function.When would someone want to do this?
For example:
def foo():
print('usually this value is inaccessible from "python land"')
def extract_printed_values(bar):
global print
old_print, returnv = print, None
def new_print(x):
nonlocal returnv
old_print(returnv := x)
print = new_print
bar()
print = old_print
return returnv
fooval = extract_printed_values(foo)
fooval will then be equal to whatever value foo printed to the console, otherwise foo will behave exactly the same as normal (assuming it only printed a single value), and print will even behave normally afterwardsIncidentally: I don't believe that is a violation of lexical scoping. f is scoped globally, inside itself f is not defined, so f(x-1) refers to the globally scoped f. When it's reassigned, you reassign the global f. Nothing here violates lexical scoping, since f is never scoped anywhere other than globally (note that the blog post never claims it is, either)
I don't disagree with Guido's larger point, though, Python probably shouldn't use TCO. The debugging/stack trace point is very true, and also it's just not Pythonic to write algorithms TCO style. You just don't need to, in Python.
And your understanding of why python doesn't permit TCE is because functions are globally scoped with indefinite extent?
No, I very much do not agree: it is still a tail call, and can use tail call optimization. When it calls f(x-1), regardless of whether f has been reassigned or not, you have to resolve what f is, because it COULD have been reassigned. The Python interpreter has to do this regardless, the fact that it's recursive is irrelevant.
You do that, and you get the function value you need to call (it could be the original value assigned to f, it could be the new value, it doesn't matter). At that point, nothing that is remaining on the stack frame except the arguments and return address is needed anymore, so you can reuse it for the the call to f(x-1). That is what tail call elimination is.
I'll grant that it's entirely possible that I've misunderstood this whole thing and that Guido is correct (he's a much smarter fella than me), but if so, I have no idea why I'm wrong.
> And your understanding of why python doesn't permit TCE is because functions are globally scoped with indefinite extent?
No, not at all, that was just a side note about why you were wrong to say it violated lexical scoping (it doesn't). My understanding of why Guido didn't do this is because he didn't think tail call elimination is a very useful feature for Python, and it destroys stack traces. Both of which I agree with. I just don't buy the other technical reason.
He goes on, later in the post, to describe how he’d go about adding tail calls - although this treatment is also confused.
This would work for every instance of call-return pairs except in try statements and similar (anything that that has whatever Python calls unwind-protect). Since, in those cases, the except/finally/else of a try is code that may be executed (or is always executed in the case of the finally) after the call but before the return.
So it really does come down to a desire to prevent the use of deep call stacks (auto-recursion is just one case that would have been optimized here) rather than a true technical limitation. This could even have been brought in incrementally over the years and provided as an option.