Search code examples
python-3.xscopeglobal-variablesdictionary-comprehension

Python:dict comprehension and eval function variable scope


Code 1: for loop

def foo():
    one = '1'
    two = '2'
    three = '3'
    d = {}
    for name in ('one', 'two', 'three'):
        d[name] = eval(name)
    print(d)

foo()

output:

{'one': '1', 'two': '2', 'three': '3'}

Code 2: dict comprehension

def foo():
    one = '1'
    two = '2'
    three = '3'
    print({name: eval(name) for name in ('one', 'two', 'three')})

foo()

output:

NameError: name 'one' is not defined

Code 3: add global keyword

def foo():
    global one, two, three  # why?
    one = '1'
    two = '2'
    three = '3'
    print({name: eval(name) for name in ('one', 'two', 'three')})

foo()

output:

{'one': '1', 'two': '2', 'three': '3'}

Dict comprehensions and generator comprehensions create their own local scope. According to the definition of the closure (or not the closure here), but why can't Code 2 access the variable one[,two,three] of the outer function foo? However, Code 3 can successfully create a dictionary by setting the variable one[,two,three] to global?

So is it because the eval function and the dict comprehensions have different scopes?

Hope someone help me, I will be grateful!


Solution

  • To understand whats happening, try this:

    def foo():
        global one
        one = '1'
        two = '2'
        print({'locals, global': (locals(), globals()) for _ in range(1)})
    
    foo()
    

    Output

    {'locals, global': ({'_': 0, '.0': <range_iterator object at ...>},
                        {'__name__': '__main__', '__package__': None, ..., 'one': '1'})}
    

    The builtin eval(expression) is a shortcut for eval(expression[, globals[, locals]]).

    As you see in the previous output, locals() is not local symbol table of the function because list/dict comprehensions have their own scope (see https://bugs.python.org/msg348274 for instance).

    To get the output you expected, you just have to pass the local symbol table of the function to eval.

    def bar():
        one = '1'
        two = '2'
        three = '3'
        func_locals = locals() # bind the locals() here
        print({name: eval(name, globals(), func_locals) for name in ('one', 'two', 'three')})
    
    bar()
    

    Output

    {'one': '1', 'two': '2', 'three': '3'}