-
-
Notifications
You must be signed in to change notification settings - Fork 31.8k
Eval/exec and comprehension scopes unclear in documentation #87771
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Python 3.9.2 seems to be giving me some unexpected difficulty evaluating generators inside evals. Here is the example: def func(l):
def get(i):
return l[i]
print(sum(get(i) for i in range(len(l)))) # works as expected, prints 10
print(eval("get(0) + get(1) + get(2) + get(3)")) # works just fine, prints 10
# if __globals is set to locals(), it still works, prints 10
print(eval("sum(get(i) for i in range(len(l)))", locals()))
# This will complain
print(eval("sum(get(i) for i in range(len(l)))"))
func([1,2,3,4]) The last line gives the following error
Any kind of generator-based code wont work. The following lines would give the same an error:
Any clue what is happening? The documentation on eval seems to give no insight on why this behavior is as is. This really feels like an issue, at the very least, it's an issue in the documentation. |
I think this is in the same class of behaviours as
Calls like In this case, inside the lambda's body the name "get" can't be resolved. For the lambda body, the name "get" is a nonlocal but there's no way to access a nonlocal in a lambda. The comprehensions, like lambdas, are in their own nested scope. |
This is not an execution bug. https://docs.python.org/3/reference/expressions.html#displays-for-lists-sets-and-dictionaries "However, aside from the iterable expression in the leftmost for clause, the comprehension is executed in a separate implicitly nested scope. This ensures that names assigned to in the target list don’t “leak” into the enclosing scope." So when the passed in locals is not the passed in globals, that means a separate local scope. https://docs.python.org/3/reference/expressions.html#generator-expressions is a little less clear. "Variables used in the generator expression are evaluated lazily when the __next__() method is called for the generator object (in the same fashion as normal generators). However, the iterable expression in the leftmost for clause is immediately evaluated, so that an error produced by it will be emitted at the point where the generator expression is defined, rather than at the point where the first value is retrieved. Subsequent for clauses and any filter condition in the leftmost for clause cannot be evaluated in the enclosing scope as they may depend on the values obtained from the leftmost iterable." By implication, the value expressions are also not evaluated in the enclosing local scope. I am thinking about adding something to the eval/exec doc, but this issue overlaps with another one about their doc. https://docs.python.org/3/reference/expressions.html#generator-expressions |
Hmm... OK, if I understand correctly, the evaluation procedure for a (e.g.) list comprehension, as described in the documentation you linked in, is as follows:
As you mention, the documentation does not properly describe in which scope is evaluated the leftmost expression that generates each object (to be placed in the list). You say that the leftmost expression is also not evaluated in the enclosing scope (which I take means that it is being evaluated in the new nested scope). But notice that sometimes it *seems to be*. There are two inconsistent behaviors:
It seems reasonable that behavior (1) should happen in both cases. In fact, I think it is *very* reasonable to expect that calling eval() on a string should have the exact same effect as if the code that is inside the eval had been written as part of the source code. Don't you think? I believe that this is exactly what happens, e.g., in Lisp. I would guess this is just a bug, but maybe there is some technical reason why this is not possible. If that is (sadly) the case, this should be explained in the documentation. Currently I don't really have a mental model for what happens when I call |
I don't think Python's execution model is defined this way. The documentation on execution model says:
https://docs.python.org/3/reference/executionmodel.html#interaction-with-dynamic-features |
I'm preparing an update to the documentation of eval/exec. There are several issues, but chiefly I'll link to the appropriate sections in the Language Reference, and clean up some other inaccuracies. When it's ready I'll submit a PR for core devs to review. |
Hmm yes, some more words in the documentation might help. Does anyone understand why it happens, though? Specifically, note that sum(get(i) for i in range(len(l))) or eval("get(0) + get(1) + get(2) + get(3)") or eval("sum(get(i) for i in range(len(l)))", locals()) work just fine, but eval("sum(get(i) for i in range(len(l)))") fails, which is really confusing. I have no mental model of what is happening that allows for the first thing to work, but disallows for the second thing. I understand it has something to do with the creation of a new subscope when the comprehension is run, but other than that, I don't really understand. Also, ideally, the last thing should work, too. I am teaching programming to college students and I could not explain to them why the first three worked but the last one failed. There was simply nothing I could say that would give them a good mental model for the execution. |
This expression inside the body of
The expression in the string doesn't introduce its own scope. The name "get" is resolved because without additional arguments, eval() gets the locals from the calling scope's locals, which is where the name "get" came.
This tells eval() to use the calling scope's locals (the value returned by the call
Without explicitly telling eval() which globals/locals namespaces to use, eval() uses the current calling scope's. This is as if it were called like eval("sum(get(i) for i in range(len(l)))", globals(), locals()) A problem arises. The generator expression in the string introduces an anonymous inner scope (let's call that scope "the box"). Inside the box, the name "i" is a local, there's no problem. But for the name "get", it's not local to the box, and it's not a global. Unlike other kinds of enclosed scope (for example, one introduced by an inner These are my attempts to explain why something works while others don't, based on my own understanding. I hope this helps somewhat, and if I made a mistake anywhere please correct them. |
Some more context: bpo-37646. The demo in that one was "eval inside list-comprehension-scope", while this one is the other way around. Perhaps another example may better illustrate the interplay between eval and the execution environment:
We get The reason is that, during compilation the compiler doesn't and cannot care about what the string "x" means as an argument to eval(). To the compiler it's just a string constant passed to a function, and it's not much different from
The compiler decides that the enclosed function g() has no locals in its block. And since there's no global with the name But the following is different:
The marked line introduces name I'm trying to think up a mental model but I'm afraid I can't find a simple one, except "once compiled, it's compiled, and eval() must learn to work with the already-compiled code". A much more in-depth description of name binding and execution in CPython is given here: especially in the section " |
Python 3.13 adds the following note to the
Comprehensions execute as if they're a nested function (with only the outermost iterable expression being evaluated in the calling scope rather than the nested scope), so this note applies to them as well. |
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields:
Linked PRs
The text was updated successfully, but these errors were encountered: