Non-local assignment is an important step on our path to viewing a program as a collection of independent and autonomous objects, which interact with each other but each manage their own internal state.

In particular, non-local assignment has given us the ability to maintain some state that is local to a function, but evolves over successive calls to that function. The balance associated with a particular withdraw function is shared among all calls to that function. However, the binding for balance associated with an instance of withdraw is inaccessible to the rest of the program. Only wd is associated with the frame for make_withdraw in which it was defined. If make_withdraw is called again, then it will create a separate frame with a separate binding for balance.

We can extend our example to illustrate this point. A second call to make_withdraw returns a second withdraw function that has a different parent. We bind this second function to the name wd2 in the global frame.

1def make_withdraw(balance):
2    def withdraw(amount):
3        nonlocal balance
4        if amount > balance:
5            return 'Insufficient funds'
6        balance = balance - amount
7        return balance
8    return withdraw
9
10wd = make_withdraw(20)
11wd2 = make_withdraw(7)
12wd2(6)
13wd(8)
Step 10 of 19
line that has just executed

next line to execute

Global
make_withdraw
 
wd
 
wd2
 
f1: make_withdraw [parent=Global]
balance20
withdraw
 
Return
value
 
f2: make_withdraw [parent=Global]
balance7
withdraw
 
Return
value
 
func make_withdraw(balance) [parent=Global]
func withdraw(amount) [parent=f1]
func withdraw(amount) [parent=f2]

Now, we see that there are in fact two bindings for the name balance in two different frames, and each withdraw function has a different parent. The name wd is bound to a function with a balance of 20, while wd2 is bound to a different function with a balance of 7.

Calling wd2 changes the binding of its non-local balance name, but does not affect the function bound to the name withdraw. A future call to wd is unaffected by the changing balance of wd2; its balance is still 20.

1def make_withdraw(balance):
2    def withdraw(amount):
3        nonlocal balance
4        if amount > balance:
5            return 'Insufficient funds'
6        balance = balance - amount
7        return balance
8    return withdraw
9
10wd = make_withdraw(20)
11wd2 = make_withdraw(7)
12wd2(6)
13wd(8)
Step 15 of 19
line that has just executed

next line to execute

Global
make_withdraw
 
wd
 
wd2
 
f1: make_withdraw [parent=Global]
balance20
withdraw
 
Return
value
 
f2: make_withdraw [parent=Global]
balance1
withdraw
 
Return
value
 
f3: withdraw [parent=f2]
amount6
Return
value
1
func make_withdraw(balance) [parent=Global]
func withdraw(amount) [parent=f1]
func withdraw(amount) [parent=f2]

In this way, each instance of withdraw maintains its own balance state, but that state is inaccessible to any other function in the program. Viewing this situation at a higher level, we have created an abstraction of a bank account that manages its own internals but behaves in a way that models accounts in the world: it changes over time based on its own history of withdrawal requests.