Yu Chen
Yu Chen

Reputation: 7440

Memory Address of Nested Functions in Python

I have always understood that with nested functions in Python, the declared inner function is created during the enclosing function's invocation, and all the names within the local scope are destroyed when the enclosing function returns.

For example, from this Real Python article,

[The local scope] is created at function call, not at function definition, so you’ll have as many different local scopes as function calls. This is true even if you call the same function multiple times, or recursively. Each call will result in a new local scope being created.

So each invocation of an enclosing function should produce a new memory address for the declared nested function, right?

def normal_function():
    def locally_scoped_function():
        pass

    print(locally_scoped_function)

normal_function() # <function normal_function.<locals>.locally_scoped_function at 0x109867670>
normal_function() # <function normal_function.<locals>.locally_scoped_function at 0x109867670>
normal_function() # <function normal_function.<locals>.locally_scoped_function at 0x109867670>

Why is the address of locally_scoped_function static? Since each invocation of normal_function should be redeclaring and re-creating the function definition, shouldn't it be stored in a different slot in memory? I expected that the print out would have a different memory address each line.

It seems I am missing something painfully obvious. I did try to look for this on StackOverflow, but surprisingly I was not able to find an answer about this specific question.

Upvotes: 0

Views: 318

Answers (1)

Barak Itkin
Barak Itkin

Reputation: 4877

As suggested in the comments by @Klaus D. - when the function is garbage collected, it's very likely the next created instance of the function will be allocated in the same address. We prove this with a simple experiment

def normal_function():
    def locally_scoped_function():
        pass
    print(locally_scoped_function)
    return locally_scoped_function

# Run as before
for i in range(3):
  normal_function()

# As before, all instances are created at the same address
<function normal_function.<locals>.locally_scoped_function at 0x7f173835f9d8>
<function normal_function.<locals>.locally_scoped_function at 0x7f173835f9d8>
<function normal_function.<locals>.locally_scoped_function at 0x7f173835f9d8>

# Let's save the created instances to avoid garbage collection
collect_to_avoid_gc = []
for i in range(3):
  collect_to_avoid_gc.append(
    normal_function()      
  )

# And indeed we'll see different addresses!
<function normal_function.<locals>.locally_scoped_function at 0x7f1737a32158>
<function normal_function.<locals>.locally_scoped_function at 0x7f1737a32378>
<function normal_function.<locals>.locally_scoped_function at 0x7f1737a32510>

Upvotes: 3

Related Questions