amigcamel
amigcamel

Reputation: 2009

Problems of **arg in Python

I'll post my code first, and then ask questions.

def menu(**arg):
    if len(arg) == 0:
        name = raw_input("Enter your name: ")
        location = raw_input("Enter your name: ")
    else:
        for i,j in arg.items():
            globals()[i] = j
    print "Name: %s | Location: %s" % (name, location)

The goal of this function is to print out something like this:

Name: someone | Location: somewhere

If I just input

menu()

OK, no problem, but if I type

menu(name='someone', location='somewhere')

something went wrong...

If I rewrite it like this:

def menu(**arg):
    if len(arg) == 0:
        pass
    else:
        for i,j in arg.items():
            globals()[i] = j
    print "Name: %s | Location: %s" % (name, location)

and I type

menu(name='someone', location='somewhere')

it worked... but I don't know why

Also, why can't I replace vars() with globals() in the "rewritten version"?

And my last question is ...
I found this code verbose and redundant..
Is there any way to make it cleaner and neater?

Thanks for your patience!

Upvotes: 2

Views: 252

Answers (3)

senderle
senderle

Reputation: 150977

First of all, don't modify globals -- that's unnecessary and overly complex. If len(args) == 0, just create a dictionary called args with a name and a location value.

The actual problem with the code, however, is that once you define location and name anywhere in the function -- even in an if clause -- Python expects that location and name will be local variables. So then when you refer to location and name, Python looks for them in the local namespace, doesn't find them, and throws an UnboundLocalError.

>>> x = 1
>>> def foo():
...     if False:
...         x = 2
...     print x
... 
>>> foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in foo
UnboundLocalError: local variable 'x' referenced before assignment

Upvotes: 11

georg
georg

Reputation: 214959

I'll answer your last question:

Is there any way to make it cleaner and neater?

For example:

def menu(name=None, location=None):
    if name is None:
        name = raw_input("Enter your name: ")
    if location is None:
        location = raw_input("Enter your location: ")
    print "Name: %s | Location: %s" % (name, location)

Upvotes: 4

Karl Knechtel
Karl Knechtel

Reputation: 61519

Instead of wondering what's wrong with the code, let Python tell you.

Let's run it:

def menu(**arg):
    if len(arg) == 0:
        name = raw_input("Enter your name: ")
        location = raw_input("Enter your name: ")
    else:
        for i,j in arg.items():
            globals()[i] = j
    print "Name: %s | Location: %s" % (name, location)

We get something like:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 8, in menu
UnboundLocalError: local variable 'name' referenced before assignment

The print line tries to use name. The exception indicates that this is a local variable, and that it hasn't been assigned. Of course the local variable hasn't been assigned, since we skipped the if block where those assignments occur, and instead ran code to update globals.

We don't want to update globals for something like this anyway; if the asked-for variables should be local, then why on earth should user-supplied variables be global? It's especially strange to take things that could have been passed in as locals, bypass that by using **args, and then forcibly stuff them into globals instead. Just doesn't make any sense.

The remaining question: why doesn't Python check globals for the name and location? Simple: the presence of code - even if it doesn't run, because this is configured at compile-time - that assigns to name and location causes the compiler to recognize those as possible locals. The local scope is tighter than the global scope, and thus is preferred.

To write code that assigns to a global (without using globals()), you need to tell Python explicitly that the name you're using should refer to a global variable, with the global keyword.

Upvotes: 3

Related Questions