6

I am trying to understand how closure works in Python, and I've found the following code snippet:

def closure():
    count = 0
    def inner():
        nonlocal count
        count += 1
        print(count)
    return inner

start = closure()
start() # prints 1
start() # prints 2
start() # prints 3

I can understand this code, as when inner function is defined, the enclosing scope has a variable named count with value 0, and the inner function will remember this value then

However, if I then move count = 0 below inner function, so the code becomes like:

def closure():
    def inner():
        nonlocal count
        count += 1
        print(count)
    count = 0
    return inner

start = closure()
start() # prints 1
start() # prints 2
start() # prints 3

To my surprise, the code still works perfectly fine, which really confuses me. As when inner is defined, the variable count doesn't exist in the enclosing scope, how could inner function is able to remember a value that doesn't exist in the namespace at this point yet?

Is it because something similar to JS's variable hoisting also exist in Python?

torez233
  • 193
  • 8

2 Answers2

3

In both of the examples, you are receiving the value returned by the closure method in start variable i.e. the method closure gets executed and returns the inner local method. So the count variable is also get defined and initialized with value 0

When you invoke inner method by using start(), that method gets executed and by that time, in both cases, the count variable is already there

But if you have code something like this, then there will be a reference error

def closure():
    def inner():
        nonlocal count
        count += 1
        print(count)
    inner()
    count = 0

start = closure()

Here inner method is called before count is defined

  • 1
    Yep and to answer the OP's other question, No Python does not have code hoisting like JS or other languages. – TheLazyScripter Sep 04 '19 at 05:35
  • "Since python is interpreter, it won't compile the method inner, until it is get invoked." => err... are you sure ? Or did you mean: "it won't compile the function `inner` before the `closure` function is invoked" ? – bruno desthuilliers Sep 04 '19 at 07:07
  • Removed the line as the term compile is tricky and confusing. Python compilation happens to the whole script and nothing to do with invocation as it happens at the execution/interpreting. Thanks for pointing it out. – Sajeer Noohukannu Sep 04 '19 at 07:50
  • your answer implies that the name's scope is resolved during runtime. That's not true, it's part of the codeobject that is defined before the code is run. Or did I misread your explanation? – Arne Sep 04 '19 at 11:42
  • Not 100% clear-cut, but this part "When you invoke inner method by using start(), that method gets executed and by that time, in both cases, the count variable is already there" to me sounds like execution order would decide scope resolution. "invoke" in particular implies to me that it would happen during runtime, not compilation. – Arne Sep 05 '19 at 06:15
  • I don't think so, I clearly mentioned in the example too – Sajeer Noohukannu Sep 05 '19 at 07:28
  • @SajeerNoohukannu The example only addresses name errors though, not scope resolution, which again is something that explicitly only happens at runtime. – Arne Sep 05 '19 at 12:19
  • I dont know what did u understand, requesting to comment on things you are 100% sure – Sajeer Noohukannu Sep 06 '19 at 04:42
  • Scoping, which is what this question is about, is resolved at compile time. Your answer, and your example in particular, deals with run-time consequences of bad scoping, i.e. name errors. While they are thematically connected, they are two distinct things, and you make them seem like the same one. – Arne Sep 09 '19 at 21:51
  • Practically speaking, it's not that bad to conflate them, so in that regard your answer is fine. I was just nit-picking about details. – Arne Sep 09 '19 at 21:54
2

From Resolution of Names:

If a name binding operation occurs anywhere within a code block, all uses of the name within the block are treated as references to the current block. This can lead to errors when a name is used within a block before it is bound. This rule is subtle. Python lacks declarations and allows name binding operations to occur anywhere within a code block. The local variables of a code block can be determined by scanning the entire text of the block for name binding operations.

(Emphasis mine.)

Assignments are name binding operations, so as long as the count = 0 exists anywhere in the function, count is considered a local variable of that function and inner will reference that variable. (Though if you call inner() before assigning a value to count, you'll get an exception.)

Aran-Fey
  • 39,665
  • 11
  • 104
  • 149