Lab 5: for-based Index Loops

Overview

We've seen before that the for loop

for val in sequence:
   ... do stuff with val ...

is syntactic sugar for the following while loop

index = 0
while index < len(sequence):
    val = sequence[index]
    ... do stuff with val ...
    index += 1

Indeed, we prefer to write for loops because it avoids the boilerplate pattern of having to initialize the index variable, test it agains the length of the sequence, extract the current value from the sequence at that index, and increment the index.

But the above translatino assume that the body of the loop,

... do stuff with val ...

just does stuff with val but not with index. But what if it also needs to refer to the index as well? E.g

index = 0
while index < len(sequence):
    val = sequence[index]
    ... do stuff with val AND index ...
    index += 1

We'd like a easy way in for loop to be able to refer to both the value and its index. We can do this via the following pattern:

for index in range(len(sequence)):
    val = sequence[index]
    ... do stuff with val AND index ...
    index += 1    

Here, we leverage the fact that range(len(sequence)) gives us all of the integers starting at 0 up to but not including len(sequence). This is exactly all of the valid nonnegative indices for sequence, which range from 0 to len(sequence)-1. This is exactly the reason why the second argument to range is not include in the sequence!

The above code is the typical structure for a for-based index loop, but it can be tweaked in many ways that address the needs of a particular problems. We'll examples of this below.

What are Index Loops Good For?

Although there are many uses for index loops, there are two compelling use cases.

Use Case 1: Accessing values at more than one index in a sequence at the same time

The simplest for loops access only one value in a sequence at a time, but often we want access more than one element at a time. The isSorted function from the Tracking Variables section is a classic example in which there's a need to use two consecutive elements of a list at the same time. From that section, we know that tracking variables are one solution to that problem. Index loops are another solution to the same problem.

Below are two alternative definitions of the isSorted function that use indices in for-based index loops to access two consecutive values at the same time.

The first version is like the tracking variable solution because it starts processing the second value in the list (at index 1) and uses index-1 to access the previous value:

def isSortedIndexPrev(nums):
    if len(nums) < 2: # Take care of the small special cases first! 
        return True
    for index in range(1, len(nums)):
        # To focus on the previous number, process the nums 
    # from index 1 to index len(nums)-1, which avoids an index
    # bug when index-1 is calculated at the first index.
        n = nums[index]               # The current num is at index i ... 
        prev = nums[index-1]          # ... and the previous num is at index-1
        if prev > n:      # If we find an inversion ...
            return False  # ... return False immediately
    return True # Return True if all numbers processed w/o finding an inversion

The second version starts processing the first value of the list (at index 0), and uses index+1 to access the next value:

def isSortedIndexNext(nums):
    if len(nums) < 2: # Take care of the small special cases first! 
        return True
    for index in range(0, len(nums)-1):
        # To focus on the previous number, process the nums 
    # from index 0 to index len(nums)-2, which avoids an IndexError
    # when index+1 is calculated at the last index. 
        num = nums[index]          # The current num is at index i ... 
        next = nums[index+1]       # ... and the next num is at index+1
        if next < num:    # If we find an inversion ...
            return False  # ... return False immediately
    return True # Return True if all numbers processed w/o finding an inversion

Organizing isSorted around the next number (rather than the previous one) is something that cannot easily be done with a tracking variable, which can only summarize values from the past rather than the future when scanning from left-to-right. (But we could instead first reverse the sequence and then effectively scan the elements from right-to-left).

Is it better to use tracking variables or index loops? For simple cases in which the loop depends on one or two previous values or some other simple summary information, Prof. Lyn tends to find that tracking variables are simpler because they avoid some common errors with getting the ranges and index calculations correct. However, index loops allow accessing any element in the list based on indices, and for some problems that's essential.

Use Case 2: Accessing values in parallel in multiple lists at the same time

An even more compelling use case of for-based index loops is to access values from the same positions in multple lists in parallel "in parallel". A simple example of this is the following printPartners function, which effectively pairs elements at the same index in two different lists:

def printPartners(seq1, seq2):
    for i in range(min(len(seq1), len(seq2))):
        print(seq1[i], seq2[i])

partners1 = ['Dani', 'Allie', 'Fiona', 'Julia', 'Penelope']
partners2 = ['Gianna', 'Keily', 'Grace', 'Sophia', 'Seona', 'Areet']

>>> printPartners(partners1, partners2)
Dani Gianna
Allie Keily
Fiona Grace
Julia Sophia
Penelope Seona

It's necessary to handle the case where the sequences have different lenghts. by taking the minimum of the two lengths, it's guaranteed that any index in the range will be valid in both sequences. This ends up ignoring the extra elements in the longer sequence.

Table of Contents