Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calling locals() in a function not intuitive?

Tags:

python

locals

This may be elementary, but may help me understand namespaces. A good explanation might step through what happens when the function definition is executed, and then what happens later when the function object is executed. Recursion may be complicating things.

The results aren't obvious to me; I would have expected:

locals_1 would contain var; locals_2 would contain var and locals_1; and locals_3 would contain var, locals_1, and locals_2

# A function calls locals() several times, and returns them ...
def func():
  var = 'var!'
  locals_1 = locals()
  locals_2 = locals()
  locals_3 = locals()
  return locals_1, locals_2, locals_3

# func is called ...
locals_1, locals_2, locals_3 = func()

# display results ...
print 'locals_1:', locals_1
print 'locals_2:', locals_2
print 'locals_3:', locals_3

Here are the results:

locals_1: {'var': 'var!', 'locals_1': {...}, 'locals_2': {...}}
locals_2: {'var': 'var!', 'locals_1': {...}, 'locals_2': {...}}
locals_3: {'var': 'var!', 'locals_1': {...}, 'locals_2': {...}}

The pattern seems to be, with (n) calls to locals, all of
the returned locals-dicts are identical, and they all include the first (n-1) locals-dicts.

Can someone explain this?

More specifically:

Why does locals_1 include itself?

Why does locals_1 include locals_2? Is locals_1 assigned when func is created, or executed?

And why is locals_3 not included anywhere?

Does "{...}" indicate an 'endless recursion'? Sort of like those photos of mirrors facing each other?

like image 873
tfj Avatar asked Apr 28 '14 15:04

tfj


Video Answer


2 Answers

Let's run this code:

def func():
  var = 'var!'
  locals_1 = locals()
  print(id(locals_1), id(locals()), locals())
  locals_2 = locals()
  print(id(locals_2), id(locals()), locals())
  locals_3 = locals()
  print(id(locals_3), id(locals()), locals())
  return locals_1, locals_2, locals_3


func()

This would be in the output:

44860744 44860744 {'locals_1': {...}, 'var': 'var!'}
44860744 44860744 {'locals_2': {...}, 'locals_1': {...}, 'var': 'var!'}
44860744 44860744 {'locals_2': {...}, 'locals_3': {...}, 'locals_1': {...}, 'var': 'var!'}

locals() here grows as expected, however you are assigning the reference to locals(), not the value of locals() to each variable.

After each assignment locals() gets changed, but reference does not, so each variable is pointing to the same object. In my output all object ids are equal, this is the proof.

Longer explanation

These variables has the same link (reference) to that object. Basically, all variables in Python are references (similar concept to pointers).

        locals_1            locals_2                 locals_3
            \                    |                      /
             \                   |                     /
              V                  V                    V
            ---------------------------------------------
            |            single locals() object         |
            ---------------------------------------------

They have absolutely no idea what value does locals() have, they only know where to get it when it's needed (when the variable is used somewhere). Changes on locals() don't affect those variables.

In the end of you function you're returning three variables, and this is what happening when you're printing them:

print(locals_N) -> 1. Get object referenced in locals_N
                   2. Return the value of that object

See? So, this is why they have exactly the same value, the one locals() has at the moment of print.

If you change locals() (somehow) again and then run print statements, what would be printed 3 times? Yep, the NEW value of locals().

like image 93
vaultah Avatar answered Sep 20 '22 20:09

vaultah


I like your question, really good one.

Yes, locals() is sort of magic, but with your approach, you will get it soon or later and will love it.

Key concepts

Dictionary is assigned by reference and not by value

In [1]: a = {"alfa": 1, "beta": 2}

In [2]: b = a

In [3]: b
Out[3]: {'alfa': 1, 'beta': 2}

In [4]: b["gama"] = 3

In [5]: b
Out[5]: {'alfa': 1, 'beta': 2, 'gama': 3}

In [6]: a
Out[6]: {'alfa': 1, 'beta': 2, 'gama': 3}

As you see, a get changed indirectly at the moment b got modified, because both a and b are pointing to the same data structure in memory.

locals() is returning dictionary with all local variables

Edit: clarified when is this dict updated

So all local variables existing at the moment of locals() call are living here. If you make subsequent calls to locals(), this dictionary is updated at the moment of the call.

Answering your questions

Why does locals_1 include itself?

Because locals_1 is reference to dictionary of all locally defined variables. As soon as locals_1 becames part of local namespace, it gets part of the dictionary locals() returned.

Why does locals_1 include locals_2? Is locals_1 assigned when func is created, or executed?

The same answer as previous one applies.

And why is locals_3 not included anywhere?

This is the most difficult part of your question. After some research I found excellent article about this topic: http://nedbatchelder.com/blog/201211/tricky_locals.html

The fact is, that locals() returns a dictionary, which contains references to all local variables. But the tricky part is, it is not directly that structure, it is a dictionary, which is only updated at the moment, locals() is called.

This explains missing locals_3 in your result. All results are pointing to the same dictionary, but it does not get updated after you introduce locals_3 variable.

When I added another print locals() before return, I found it there, without it not.

Uff.

Does "{...}" indicate an 'endless recursion'? Sort of like those photos of mirrors facing each other?

I would read it as "there is something more". But I think, you are right, that this is a solution for printing recursive data structure. Without such a solution the dictionary could not be really printed in finite amount of time.

Bonus - use of **locals() in string.format()

There is one idiom, where locals() are shortening your code a lot, in string.format()

name = "frost"
surname = "national"
print "The guy named {name} {surname} got great question.".format(**locals())
like image 39
Jan Vlcinsky Avatar answered Sep 22 '22 20:09

Jan Vlcinsky