If at first you don't succeed...

About | Archive

Lists and Nonlocal in Python

Mutable closures

A closure is a function with a parent frame that contains some data. If the parent frame can be modified, the function is said to be mutable.

For example, take the following doctest:

>>> c, d = make_counter(), make_counter()
>>> c()
0
>>> c()
1
>>> c()
2
>>> [c(), d(), d(), d(), c()]
[3, 0, 1, 2, 4]

The c is called multiple times and returns a different value every time; but its state isn’t global as there is the d function which also counts up but on its own tineline.

The Python programming language allows for the creation of mutable closures in two different ways: one that is traditionally considered “functional” and one that is traditionally considered “object-oriented”. Let’s take a look at them now:

Nonlocal: Functional mutable closures

The most obvious way to implement the make_counter function is as follows:

def make_counter():
    current_value = 0
    def c():
        nonlocal current_value
        result = current_value
        current_value += 1
        return result
    return c

The line nonlocal current_value ensures that the current_value variable assigned in the c frame refers to the value from the parent frame. Thus we have shared state (the current_value variable) which can be modified, the necessary components of a closure.

Lists: Object-oriented mutable closures

But do we really need anything that complicated? What if we did this?

def make_counter():
    return [6, 5, 4, 3, 2, 1, 0].pop

Every time we call the resulting function, we get 0, 1, 2, 3, 4, 5, and then 6. This isn’t quite right, as it doesn’t go on forever, but it’s definitely a mutable function, and we never used nonlocal. What’s going on here? Let’s unroll the function somewhat:

def make_counter():
    lst = [6, 5, 4, 3, 2, 1, 0]
    def c():
        return lst.pop()
    return c

Here we have that we can modify lst from the parent frame without assigning to it, since the contents of lst rather than the variable itself is mutated. We can exactly duplicate the original make_counter function as such:

def make_counter():
    current_value = [0]
    def c():
        result = current_value[0]
        current_value[0] += 1       # looks like an assignment
        return result
    return c

Now, this looks like there’s an assignment to current_value on the commented line, but in fact, current_value[0] += 1 is equivalent to current_value[0] = current_value[0].__iadd__(1), which is equivalent (for numbers) to current_value[0] = current_value[0] + 1, and that is equivalent to current_value.__setitem__(0, current_value[0] + 1), which is not in fact an assignment.

Complications: Nonlocal on lists

Let’s say we write a few alternatives for make_counter:

def make_counter_append():
    current_value = [0]
    def c():
        result = current_value[-1]
        current_value.append(current_value[-1] + 1)
        return result
    return c

def make_counter_extend():
    current_value = [0]
    def c():
        result = current_value[-1]
        current_value.extend([current_value[-1] + 1])
        return result
    return c

def make_counter_plus_equals():
    current_value = [0]
    def c():
        result = current_value[-1]
        current_value += [current_value[-1] + 1]
        return result
    return c

Interestingly, the first two work but the last does not. Why? Aren’t a += b and a.extend(b) equivalent? Actually, they’re almost equivalent. a += b is equivalent to a = a.__iadd__(b), where __iadd__ is a special function that you can choose to implement on your class. In the case of Python, list.__iadd__ is equivalent to list.extend except that it returns a reference back to the same list.

Therefore, if you want to use a += b where a is a list, you thus need either to make a nonlocal or use a.extend(b) instead.

comments powered by Disqus