Search code examples
pythonlocals

Calling locals() in a function not intuitive?


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?


Solution

  • My original question settles down to, 'just what is locals()?' Here's my current (speculative) understanding, written in Pythonese:

    ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    What is the nature of Python's locals() ?

    Every local namespace has it's own namespace-table, which can be viewed in total using the locals built-in function. (A namepace-table, in effect, is like a dict that holds "identifier": object entries; for each item, the key is the name (in string-form) assigned (or 'bound') to the object.)

    When called in a non-global level, locals returns the interpreter's sole representation of the current local namespace-table: a 'dynamic', always-up-to-date, specialized, dict-like object.

    It's not a simple dict, nor is it the actual name-table, but it's effectively 'alive', and is instantly updated from the live table anytime it is referenced (when tracing is on, it updates with every statement).
    On exiting the scope, this object vanishes, and is created anew for the current scope whenever locals is next called.

    (When called in a global (modular) level, locals instead returns globals(), Python's global-namespace representation, which may have a different nature).

    So, L = locals() binds the name L to this 'stand-in' for the local namespace-table; subsequently, anytime L is referenced, this object is refreshed and returned.
    And, any additional names bound (in the same scope) to locals() will be aliases for this same object.

    Note that L, being assigned to locals(), necessarily becomes an 'infinite recursion' object (displayed as the dict {...}), which may or may not be important to you. You can make a simple dict copy of L at any time, however.
    Some attributes of locals(), such as keys, also return simple objects.

    To capture a 'pristine' snapshot of locals() within a function, use a technique that doesn't make any local assignments; for example, pass the copy as argument to a function, that pickles it out to a file.

    There are particulars about L, and how it behaves; it includes free variables from the blocks of functions, but not classes, and, the docs warn against trying to alter L's contents (it may no longer 'mirror' the name-table).
    It perhaps should only be read (copied, etc.)

    (Why locals() is designed to be 'live', rather than a 'snapshot', is another topic).

    In summary:

    locals() is a unique, specialized object (in dict-form); it's Python's live representation of the current local namespace-table (not a frozen snapshot)

    ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    A way to get the results I had expected, then, is to produce copies of locals() (here using dict.copy), at each step:

    # A function copies locals() several times, and returns each result ...
    def func():
        var = 'var!'
        locals_1 = locals().copy()
        locals_2 = locals().copy()
        locals_3 = locals().copy()
        return locals_1, locals_2, locals_3
    

    func is called, and the returns are displayed:

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

    The returns are simple dict objects, that capture the growing stages of the local namespace.
    This is what I intended.

    Other possible ways to copy locals() (here "L") are dict(L), copy.copy(L) and copy.deepcopy(L).