Nonlocal Practice Problems

Basic Concepts

  1. Summarize in your own words what the nonlocal keyword does, and what problem nonlocal tries to solve. Modify this definition accordingly as you get more practice down in the next few sections.

    nonlocal allows names to refer to those in parent frames instead of the current local frame, which can be useful when one wants to keep state (remember that the local frame disappears once the function is returned) but does not want it in the global frame.

    Remember that for assignment, if Python does not see the name in the current frame, it will try to add that name to the current frame. Therefore, assignment without nonlocal will not modify the one in the parent frames the user wants to modify.

    Think about make_withdraw: you want to keep a user's balance private, right? You do not want anyone with access to the global frame to modify the balance. Simple examples like these will help with remembering the purpose of nonlocal.

    This can be useful when you have an operation that has to keep state, but designing a full class for it seems like overkill. For instance, the animate_leaf function in ants_gui.py uses nonlocal to keep track of where to start drawing the leaf. The starting point changes for the duration of the operation because the drawing is meant to move.

    Thus, to reiterate, nonlocal allows users to declare names not in the current frame but in the parent frames (excluding global) such that they can be re-assigned without adding them to the current frame.

  2. How does nonlocal relate to the general discussion of object mutation? Compare and contrast what a programmer tries to do in nonlocal versus in object mutation.

    One can think of nonlocal as modifying bindings (assignments) in parent frames. Current Python syntax without nonlocal does not allow for this modification, as discussed above. Meanwhile, objects are free to be mutated. With this syntax extension, users can now mutate both things: assignments and the objects themselves.

  3. Does nonlocal work on names in the global frame? Why/why not?

    No, because that would defeat the purpose of choosing a name from a parent frame such that the name is enclosed, unable to be modified in the global frame. Recall the make_withdraw example. There is, in fact, the global keyword for that purpose.

  4. Give the simplest example you can that uses nonlocal correctly.

    The simplest example is an operation that keeps track of a number, so that one can increment it. `def make_counter():

     count = 0
     def counter():
         nonlocal count
         count += 1
         return count
     return counter
    
  5. Does nonlocal treat parameter names differently from regular names?

    Nope. An environment diagram clearly shows that there is really no difference in names.

  6. Describe explicitly the lookup procedure nonlocal does when the keyword is encountered.

    nonlocal starts looking in the most immediate parent frame for the declared name. It would not make sense for nonlocal to start in the current frame. If not there, nonlocal will keep going up a parent frame and stop at the last parent frame before the global frame.

Experimentation

Now that you have a good understanding of nonlocal, it's time to understand misconceptions about nonlocal. Try simple examples with nonlocal where the code fails. (If you really can't think of examples, revisit Discussion 7.)

  1. What does the error message say?
  2. Given what you know about the motivation, why do you think Python does not allow what you just did?
In [1]:
x = 5
nonlocal x
<input>:2: SyntaxWarning: name 'x' is assigned to before nonlocal declaration
<input>:2: SyntaxWarning: name 'x' is assigned to before nonlocal declaration
<input>:2: SyntaxWarning: name 'x' is assigned to before nonlocal declaration

  File "<ipython-input-1-a1fa932b5897>", line 2
    nonlocal x
SyntaxError: nonlocal declaration not allowed at module level

Ignore the SyntaxWarnings (even IPython knows that this code is a bad idea). Using nonlocal in the global frame truly misses the purpose of nonlocal which is to modify names in an enclosing scope: parent frames that are not global so that those names can be accessed only from within the function.

In [3]:
count = 0
def counter():
    nonlocal count
    count += 1
    return count
counter()
  File "<ipython-input-3-5cb163cab6da>", line 3
    nonlocal count
SyntaxError: no binding for nonlocal 'count' found

count is not nonlocal: count is global. Again, think back to the purpose of having nonlocal in the first place, and you can see why Python does not let you use nonlocal here to modify a global variable.

In [5]:
def counter(x):
    nonlocal x
    x += 1
    return x
  File "<ipython-input-5-637b2d3beeff>", line 2
    nonlocal x
SyntaxError: name 'x' is parameter and nonlocal

Here, the parameter has to exist in the local frame, so it cannot possibly be nonlocal. (As a consequence of the syntax, nonlocal names cannot exist in the current frame. This is where the UnboundLocalError comes from).

In [7]:
def make_counter():
    count = 0
    def counter():
        count += 1
        return count
    return counter
a = make_counter()
a()
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-7-da4d0b377715> in <module>()
      6     return counter
      7 a = make_counter()
----> 8 a()

<ipython-input-7-da4d0b377715> in counter()
      2     count = 0
      3     def counter():
----> 4         count += 1
      5         return count
      6     return counter

UnboundLocalError: local variable 'count' referenced before assignment

So you think about recreating an UnboundLocalError and here it is. Without nonlocal, Python assumes that you are creating a new name count in the current frame. You cannot refer to the same name nonlocally and locally, or else it causes major confusion with Python's other built-in behavior. Also, Python cannot magically read your intentions, so if you intend to refer to a nonlocal name, you should declare it.

Environment Diagrams

Once you are familiar with the motivation behind nonlocal, you can generate your own environment diagram questions! As always, you can check your answers on PythonTutor, but make sure that you have it on Python 3.

In []:
def make_draw(tickets):
    quantity = tickets
    def draw():
        nonlocal quantity
        quantity -= 1
        return quantity
    return draw
drawing = make_draw(100)
drawing()
drawing()
draw()

For nonlocal problems, it helps to write down what you want to keep track. In this case, we want to keep track of the quantity -- the number of tickets -- and the inner function decrements from the number of tickets each time.

In []:
def mutate_two(lst1, lst2):
    curr_lst = lst1
    def mutate(x):
        nonlocal curr_lst
        curr_lst.append(x)
        curr_lst = lst2 if curr_lst is lst1 else lst1
    return mutate
lst1, lst2 = [1], [100]
mutator = mutate_two(lst1, lst2)
mutator(10)
mutator([3])
mutator('hello')

Here, we are keeping track of which list we append to using curr_lst and nonlocal gives us the ability to reassign this name so that we can alternate which list to append to each time.

In []:
def fn(a, b, c):
    start_letter = 0
    remote = True
    def randomize():
        nonlocal a, b, c, start_letter
        a, b, c = b, c, a
        start_letter += start_letter % 3
        if remote:
            print(a)
    return randomize
strings = fn("i", "am", "Python")
strings()
strings()

We are keeping track of many things here: the arguments passed in that we will rotate, and the start_letter variable, which tells us which letter we are on. Notice that if you do not assign to a nonlocal name, you do not have to declare the name to look it up. This can be seen with the remote name.

For the following, you will not see anything as elaborate on the exam, but solving this diagram systematically (i.e. by following all the rules) will be great practice. It also introduces you to visualizing dictionaries in environment diagrams.

In []:
def bart_operator():
    location = "Berkeley"
    def change_loc(loc):
        nonlocal location
        location = loc
        print(loc)
    tasks = {'maintain': lambda car: 'Fixed car!', 'move': change_loc, \
             'call': lambda msg: print(msg + '? You got it.')}
    task = tasks['call']
    def work(status, arg):
        nonlocal task
        if status == "do":
            return task(arg)
        elif status == "change":
            if arg in tasks:
                task = tasks[arg]
            else:
                return "idk how to do that"
    return work
dickson = bart_operator()
dickson("do", "Fix wires")
dickson("change", "move")
dickson("do", "Union City")

In this problem, we are keeping track of which task function the operator is currently prepared to do. The inner function that is returned is a dispatch function that will allow us to change the function assigned to task. Note how this is getting more and more similar to classes and instances. At some point, it may be a better idea to use OOP instead of nonlocal: nonlocal is usually a quick way to allow operations (functions) to store state but it is not the most complete solution.

Afterword

For those of you who like to read about how nonlocal came to be, PEP 3014 describes completely the discussion Python developers have had over nonlocal and the rationale behind this new construct. In general, you can look for documentation using the command pydoc3 <built-in word> in the command line. e.g. pydoc3 nonlocal or pydoc3 global