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
- Lab 5 Home
- Part 1: General Important Style Guidelines
- Part 1: Early Returns from Loops in a Function
- Part 2: Other Early Exits from Loops
- Part 3: Tracking Variables
- Part 4: for-based Index Loops
- Part 5: Advanced
forLoop Exercises - Part 6: Debugging Loops
- Part 7: Loops and Graphics(optional)