Search code examples
pythonpython-3.xpython-2.7execlocal-variables

How to use exec to assign list to variable according to dictionary in Python3?


I want to assign list to variable, while list and variable are both stored in a dictionary measurements. This is the Python 2 version of measurements with less variables:

measurements = {'tg': [8.184e+16, 8.345e+16, 8.045e+16, 8.520e+16, 8.322e+16, 7.622e+16, 4.305e+16, 2.203e+16]}

def f():
    for key,val in measurements.items():
        exec(key + ' = val') in globals(),locals()
    print (tg)
f()

However, as mentioned in another question, it's not suitable for Python 3. If I write the code like this:

measurements = {'tg': [8.184e+16, 8.345e+16, 8.045e+16, 8.520e+16, 8.322e+16, 7.622e+16, 4.305e+16,
 2.203e+16]}

def f():
    for key,val in measurements.items():
        ldict = {}
        exec(key + ' = val', globals(),ldict)
        key = ldict[key]
        # exec(key + ' = val') in globals(),locals()
    print (tg)
f()

I got this error: NameError: name 'val' is not defined


Solution

  • The ldict = {} trick creates a substitute local namespace for use inside exec. This is useful because the dict returned by locals() doesn't write through to your actual locals like it used to in Python 2.

    But that substitute namespace {} is empty. It doesn't contain your locals(), therefore it doesn't have val in it. Try using ldict = {**locals()} instead to copy the contents of your locals to the substitute locals ldict.


    Remember that you have to read all the "locals" created by exec from the ldict. Thus, print(tg) won't work either, because it was only ever assigned in one of the substitute local namespaces. You probably don't want to make a new one every loop. Just .update() one you make in advance.

    def f():
        ldict = {}
        for key,val in measurements.items():
            ldict.update(locals())
            exec(key + ' = val', globals(),ldict)
            key = ldict[key]
            # exec(key + ' = val') in globals(),locals()
        print (ldict['tg'])
    

    The number and names of locals must be known in advance by the compiler in Python3 for performance optimizations. (This doesn't apply to globals(), they still write through.)

    If you know them in advance, you can just assign from them, e.g.

    tg = ldict['tg']
    print(tg)
    

    If you need more than one you could unpack a dictionary into locals, like

    a, b, c = (ldict[key] for key in ['a', 'b', 'c'])
    

    Or you could dump the whole dict into a simple namespace and access them with . instead of [].

    from types import SimpleNamespace
    
    # ...
    
    ns = SimpleNamespace(**ldict)
    print(ns.tg)
    

    You could also just exec any code that needs the new locals, since you can give exec the ldict namespace.

    exec("print(tg)", globals(), ldcit)
    

    I understand that your example code may be simplified from the original, but it does not appear to need exec at all. It is generally considered bad form to use exec unless you absolutely need it, since it confuses static analysis tools and compiling strings at runtime is slow, especially if repeated in a loop like that.

    If you must use exec, it's better to put the loop inside the exec string (use triple quotes) than the exec call inside the loop. That way the string only has to be compiled once, instead of for each loop.