0

What is the reason that the nested function in the first example using a regular variable can't change the variable in the outer scope, while the one in the second example using an item in a list can?

I am looking for an answer from language design perspective.

Thanks.

Example one:

def make_counter():
    count = 0
    def counter():
        count += 1
        return count
    return counter

Example two:

def make_counter():
    count = [0]
    def counter():
        count[0] += 1
        return count[0]
    return counter

2 Answers 2

3

This is because integers in python are imutable, while lists are not.

Hence, when you write count += 1, this is equivalent to

count = count + 1

which creates a new variable (which is then local to counter), in contrast, when you write count[0] += 1 this is equivalent to

value = count[0]
value = value + 1
count[0] = value

here, count has not been re-assigned to any new value, and you modify the original count, created outside the inner function, by assigning the (newly created) value to it.

Sign up to request clarification or add additional context in comments.

Comments

2

Python closures work exactly like this.

All free variables (i.e. variables which are neither local vars, nor arguments) are saved in the internal __closure__ property.

In [1394]: def make_counter():
      ...:     count = 0
      ...:     def counter():
      ...:         nonlocal count
      ...:         count += 1
      ...:         return count
      ...:     return counter
      ...: 
In [1395]: f = make_counter()

In [1396]: f
Out[1396]: <function __main__.make_counter.<locals>.counter>

In [1397]: f.__closure__
Out[1397]: (<cell at 0x10af4bd98: int object at 0x10024ac20>,)

In [1399]: f.__closure__[0].cell_contents
Out[1399]: 0

It just so happens that, in the first case, ints are immutable, so you are not modifying the original count.

When f is called, the inner function has its own environment frame is created which contains local variable count and is merged with __closure__.

Try calling f, and the counter is incremented:

In [1400]: f()
Out[1400]: 1

In [1401]: f.__closure__[0].cell_contents
Out[1401]: 1

In the latter case, you're modifying an entry in the list, which is mutable. The reference is copied into the __closure__ property.

By the way, make_counter is a global function, and so has an empty __closure__ attribute:

In [1403]: print(make_counter.__closure__)
None

Your second example, modified a bit:

In [1404]: def make_counter():
      ...:     count = [0]
      ...:     print(id(count))
      ...:     def counter():
      ...:         nonlocal count
      ...:         count[0] += 1
      ...:         return count[0]
      ...:     return counter
      ...: 

In [1405]: f = make_counter()
4495587464

In [1408]: id(f.__closure__[0].cell_contents)
Out[1408]: 4495587464

The id is identical.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.