As mentioned in my last post, while working on a lambda calculus interpreter, I encountered an issue where I wanted to skip ahead in a for loop, but found that I couldn’t. Although I initially put it aside, when I was getting a code review on the interpreter from Tom, I remembered the issue again and did some further investigation on the topic.
Reassigning the Index Variable
It turns out that unlike for loops in Java or C++, the index variable (e.g. i
in for (int i = 0; i < 10; i++)
is drawn directly from the object following the in
keyword each time the for loop goes around. As such, changing what the index i
references in the equivalent Python loop for i in range(10)
won’t affect the next round of the for loop (though it will affect anything following the reassignment in the block).
1 2 3 |
|
The above code results in:
1 2 3 4 5 |
|
Reassigning the Iterable Variable
Additionally, after you provide Python with an initial reference to an object with an iterable method in a for loop, Python accesses the object directly without referring back to the reference variable passed into the for loop. So, what does this mean? Well, it means that you can’t reassign the for loop’s iterable object in the middle of a for loop.
1 2 3 4 |
|
The above code, despite reassignment of iter_this
results in:
1 2 3 4 5 |
|
So, does this mean that you can’t do anything to modify what a for loop ranges over once you’ve started it? Nope. Although Python accesses the iterable object directly without regards to what your reference subsequently points to, something else can also access that iterable object — specifically, the reference that you originally used to direct Python to that object.
Mutating the Iterable Object
Since you have a reference to the Python object itself, you can actually change the object that is being iterated over by the for loop from right under it. Essentially instead of reassigning the object’s reference, you can change the object itself, i.e. mutate it.
We know what the below code does:
1 2 3 |
|
1 2 3 4 5 |
|
However, this does something different:
1 2 3 4 |
|
1 2 3 |
|
So what happened? Well, let’s take a closer look:
1 2 3 4 5 6 |
|
1 2 3 4 5 6 7 |
|
As the for loop iterates through the list created by the range
function, we mutate the list by popping off its last element at the end of each for loop round. Hence, the list that the for loop is drawing from is getting shorter as the for loop lists, resulting in the for loop ending early.
This means that if you need to skip elements in a for loop, you can certainly mutate the iterable object. However, just because you can doesn’t mean you necessarily should. Mutation will often cause the for loop to become a lot less straightforward to reason about and may end up creating strange and confusing bugs.
Other Ways to Skip Around a Loop
One way of skipping elements in a loop that isn’t quite as opaque is by explicitly creating another variable that can be modified and continue
ing when the elements in the iterable are less than (or greater than, or whatever) than the modifiable variable.
This method is what I ultimately settled on for parts of my lambda calculus interpreter that needed to skip elements. It made sense since my for loop skipping would always be in the forward direction (I would never need to backtrack in my for loop) and this makes it crystal clear what I’m doing.
1 2 3 4 5 6 7 8 9 |
|
1 2 3 4 |
|
On the other hand, if you need to skip around a loop (backwards as well as forwards), it is probably better (and clearer) to just use a while loop.
1 2 3 4 5 6 7 8 9 |
|
1 2 3 4 5 6 |
|