0

I wanted to receive some arguments via **kwargs and convert them to local variables. So I first tried like this:

for k, v in kwargs.items():
    cmd = k + ' = ' + stringify(v)
    exec(cmd)

Which ... somehow did not work. Trying to figure out why, at some point I had this example:

def stringify(s):
    if isinstance(s, str):
        return "'" + s + "'"
    else:
        return str(s)


def print_var(local):
    print(local)


def test_func(**kwargs):
    for k, v in kwargs.items():
        cmd = k + " = " + stringify(v)
        print(cmd)
        exec(cmd)
    print()

    print("locals: " + str(locals()))
    print()

    print("--- locals()['var']:")
    print(locals()["foo"])
    print(locals()["bar"])
    print(locals()["baz"])
    print()

    print("--- eval")
    print(eval("foo"))
    print(eval("bar"))
    print(eval("baz"))
    print()

    print("--- exec")
    exec("print_var(foo); print_var(bar); print_var(baz)")
    print()

    try:
        foo
    except NameError as e:
        print("NameError:", e)

    try:
        print_var(foo)
    except NameError as e:
        print("NameError:", e)

    try:
        bar
    except NameError as e:
        print("NameError:", e)

    try:
        print_var(bar)
    except NameError as e:
        print("NameError:", e)

    try:
        baz
    except NameError as e:
        print("NameError:", e)

    try:
        print_var(baz)
    except NameError as e:
        print("NameError:", e)


if __name__ == "__main__":
    kwargs = {"foo": 13, "bar": True, "baz": "whatever"}
    test_func(**kwargs)

When executing, I get the following result (Python 3.6.9 in a virtualenv)

foo = 13
bar = True
baz = 'whatever'

locals: {'cmd': "baz = 'whatever'", 'v': 'whatever', 'k': 'baz', 'kwargs': {'foo': 13, 'bar': True, 'baz': 'whatever'}, 'foo': 13, 'bar': True, 'baz': 'whatever'}

--- locals()['var']:
13
True
whatever

--- eval
13
True
whatever

--- exec
13
True
whatever

NameError: name 'foo' is not defined
NameError: name 'foo' is not defined
NameError: name 'bar' is not defined
NameError: name 'bar' is not defined
NameError: name 'baz' is not defined
NameError: name 'baz' is not defined

And ... I don't get why. Why are the variables not available locally, even though they seem to be in the locals, and can be used by exec and eval.

This answer suggests a way if I'm okay with using eval - Though I'd like to avoid using it.

It was mentioned here that locals are optimized as an array at runtime in Python 3. The linked thread gives slightly more clarity about it as well - though according to it, locals will not be modified. However, this clearly (?) happened here.

So my assumption is: for some reason, two locals exist, one is probably an array and was created at 'compile'-time, and the other one (the one I'm modifying with exec) is probably an actually dynamic dict. And that while the new, dynamic one is accessible from eval and exec, for some reason it will not be considered when trying to resolve variables 'normally'.

So I guess my questions are:

  • Where did I go wrong?
  • Which part of 'local variable' did I misunderstand? How is it different in modules?
  • Where can I find more information about this behaviour?
  • Is there a different way to use variables passed via **kwargs as local variables without resorting to eval when calling them?

Help is appreciated!

fkarg
  • 198
  • 2
  • 17
  • Your attempts at printing your `exec`-created variables is failing because the variable references are being interpreted as globals (which genuinely don't exist). A variable reference cannot be compiled as a reference to a local unless such a local exists at compile time. – jasonharper Jun 22 '20 at 14:53

1 Answers1

0

You can extract the **kwargs variables from a method call with something like this:

def printfoo(foo=None, **kwargs):
    print(f"foo is {foo}")

def printa(a=None, **kwargs):
    print(f"a is {a}")

def printall(**kwargs):
    printfoo(**kwargs)
    printa(**kwargs)

print("Only foo variable is set:")
printall(foo="bar")
printall(**{"foo": "bar"})

print("\nOnly a variable is set:")
printall(a=5)

print("\nfoo and a variable are set:")
printall(**{"foo": "barrrrrr", "a":999})

Resulting output:

Only foo variable is set:
foo is bar
a is None
foo is bar
a is None

Only a variable is set:
foo is None
a is 5

foo and a variable are set:
foo is barrrrrr
a is 999

In the code I directly address the variables by their name foo and a (in the f-strings).

Tin Nguyen
  • 5,250
  • 1
  • 12
  • 32
  • Your approach certainly works - and while I will probably do it like this (If no other answer shows up), I was looking for a 'more direct' way - without having to add `**kwargs` to all functions down the line. Thanks for your help! – fkarg Jun 22 '20 at 15:30