L8Cod3r
L8Cod3r

Reputation: 92

Python: How to let eval() see local variables?

I have the following:

x = [1,2,3,4,5]
def foo(lbd:str, value):
    ret_val = eval(lbd, globals(), locals())
    print(ret_val)

using 'value' variable in this call succeeds:

>>> foo("[i for i in value]",x)
            
[1, 2, 3, 4, 5]

but this one fails:

>>> foo(r"any([x in value for x in {'',0,None,'0'}])", x)
            
Traceback (most recent call last):
  File "<pyshell#171>", line 1, in <module>
    foo(r"any([x in value for x in {'',0,None,'0'}])", x)
  File "<pyshell#165>", line 2, in foo
    ret_val = eval(lbd, globals(), locals())
  File "<string>", line 1, in <module>
  File "<string>", line 1, in <listcomp>
NameError: name 'value' is not defined

I am able to work around this, but curious to know what's going on here.

>>> foo(r"(lambda V=value: any([x in V for x in {'',0,None,'0'}]) )()", x)
False

Upvotes: 3

Views: 1390

Answers (2)

juanpa.arrivillaga
juanpa.arrivillaga

Reputation: 95872

This is a super subtle point. So, if you read the documentation for eval, it doesn't mention the case where you provide arguments for both globals and locals, but I am fairly certain it works the same as for exec:

If exec gets two separate objects as globals and locals, the code will be executed as if it were embedded in a class definition.

In class definitions, functions don't get access to their enclosing scope. So this is exactly the same as the error:

>>> class Foo:
...     value = [1,2,3]
...     print([x in value for x in [2,4,6]])
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in Foo
  File "<stdin>", line 3, in <listcomp>
NameError: name 'value' is not defined

Because list comprehensions work by creating a function object underneath the hood. This is also why you need self.some_method to access the names of other methods defined in your class. More about the above in the excellent accepted answer here.

So it's the same as:

>>> def foo():
...     x = 3
...     return eval('(lambda: x + 1)()', globals(), locals())
...
>>> foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in foo
  File "<string>", line 1, in <module>
  File "<string>", line 1, in <lambda>
NameError: name 'x' is not defined

However, this works just fine:

>>> def foo():
...     x = 3
...     return eval('x + 1', globals(), locals())
...
>>> foo()
4

Because there is no (non)enclosed function scope involved.

Finally, the reason that the following works:

>>> def foo():
...     values = [1,2,3]
...     return eval('[x+2 for x in values]', globals(), locals())
...
>>> foo()
[3, 4, 5]

Is because the iterable in the left-most for-clause of a comprehension gets evaluated not in the function scope of the comprehension but in the scope of where the comprehension occurs (it is literally passed as an argument). You can see this in the dissasembly of a list comprehension:

>>> import dis
>>> dis.dis('[x+2 for x in values]')
  1           0 LOAD_CONST               0 (<code object <listcomp> at 0x7fe28baee3a0, file "<dis>", line 1>)
              2 LOAD_CONST               1 ('<listcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_NAME                0 (values)
              8 GET_ITER
             10 CALL_FUNCTION            1
             12 RETURN_VALUE

Disassembly of <code object <listcomp> at 0x7fe28baee3a0, file "<dis>", line 1>:
  1           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                12 (to 18)
              6 STORE_FAST               1 (x)
              8 LOAD_FAST                1 (x)
             10 LOAD_CONST               0 (2)
             12 BINARY_ADD
             14 LIST_APPEND              2
             16 JUMP_ABSOLUTE            4
        >>   18 RETURN_VALUE

Note, values is evaluated, iter is called on it, and the result of that is passed to the function:

              6 LOAD_NAME                0 (values)
              8 GET_ITER
             10 CALL_FUNCTION            1

The "function" is basically just a loop with append, see the: Disassembly of <code object <listcomp> at 0x7fe28baee3a0, file "<dis>", line 1> for how list comprehensions do their work.

Upvotes: 8

Woodford
Woodford

Reputation: 4439

In addition to @juanpa.arrivillaga's answer, there is some discussion of this behavior in this bug report (it's not a bug).

Here's a quick fix for your immediate question:

x = [1,2,3,4,5]
def foo(lbd:str, value):
    ret_val = eval(lbd, {'value': value})
    print(ret_val)

>>> foo(r"any([x in value for x in {'',0,None,'0'}])", x)
False

Upvotes: 4

Related Questions