Lesson 7: Loops and Lists (and other sequences)

We've covered how to make decisions; now it's time to handle loops. Looping is a necessity in many programs, as you'll often be asking for user input, then acting on it, then going back to asking for input again. In more complex programs, like games, you'll be running on a timer that periodically checks for input, and acts on both that input and its own internal state, many times per second. And, since you'll be making at least one game by the end of this course, it's vital that we learn how these loops work.

There are two ways to make a loop, the while statement and the for statement. The simpler of these two is the while statement. First, though, let's cover something important.

Escaping Infinite Loops

You are going to eventually get your code stuck in an infinite loop. This is not a mere possibility, this is a certainty. When you get stuck, you need to know how to get out of it. Fortunately, it's not too hard.

If you're in the Python REPL or command line python3, you can stop the script by pressing Ctrl-C at the same time. This won't kill the REPL, it just kills that line of execution. You can also do this with command line scripts you run.

If you're running a GUI application in Python, and the program gets stuck in an infinite loop, you'll likely have to kill it using your OS's equivalent to the Task Manager (Ctrl-Alt-Del) or Force Quit Dialog (Cmd-Option-Escape) and selecting the application, then telling it to stop with a button press.

Whenever possible, be careful with your loops and make sure they can properly exit rather than getting stuck forever.

With that out of the way, on to the statements!

while statements

A while statement takes a single condition, same as an if statement. Of course, this statement can be composed of a bunch of boolean expressions with operators, same as for an if statement once again. The while statement is followed by a block, which will execute if the condition is true, and will be skipped if it is false. After a while statement's block executes, the program will go back to where the while statement starts, and check the condition again. If the condition is still true, the loop continues on like this.

Thus, the code in the block will execute "while" the condition is true, and will only stop when the condition becomes false.

s = input('Say "python": ')
while s != "python":
s = input('No. I said, Say "python": ')

The above program will keep prompting the user for input until they type in the word python, in all lower case.

You can easily create a condition that will always be true, by typing

while True:
pass

This loop will never end. (The keyword "pass" by itself on a line creates a line that does nothing; we have to have this here or the while loop would be an error.) So, how do we escape from such a while loop? You use the "break" statement.

while True:
s = input('Say "python": ')
if(s != "python"):
print("No, I said...")
else:
break

The above program does almost the same thing as the previous "Say python" program, but the text is changed a little.

The break statement breaks out of the smallest enclosing loop, and works with for and with while loops.

Another statement useful for both for and while loops is the continue statement. This continues on to the next iteration of the loop; that is, it skips over any other code in the block and goes back to the top of the smallest enclosing loop, and repeats the condition check.

while True:
s = input('Say "python": ')
if(s == '"python"'):
print("Without the quotes, please.")
continue
if(s != 'python'):
print("No, I said...")
continue
break

The above still does more or less what the other programs do, but now it adds an option to check if the user typed "python" including the quotes, as a small easter egg. While this could have been done with a simple if elif else block, there are more complicated cases you might run into in your code where the if elif else option would be annoying to deal with. For those cases, the continue statement is here for your use.

The main difference between while loops and for loops is that while loops check a condition, but for loops iterate over a sequence. One type of sequence that works is a string; if you use a string, the for loop will go through the string character by character. However, there are other kinds of sequences which we need to cover before explaining further.

Lists and Tuples

There are two main types of sequence, Lists and Tuples. Lists are mutable, which means you can change their contents at any time, inserting or removing items, and swapping items out. Tuples are immutable, so their sequence of objects may not change. If the objects are themselves mutable, however, they can change their contents; thus, you can have a tuple that contains lists, and the lists can be changed inside.

Lists are created by writing a sequence of items in [] square brackets, separated by commas.

l = [1, 2, 3]

Before we begin, note that all of the rules we cover for accessing lists also work for all other sequence types, including strings and tuples. The only difference is you can't alter the contents of a tuple sequence, so no adding or removing items.

Lists can be accessed through indexes, by writing the name of the list, followed by a number in [] square brackets.

>>> l[0]
1
>>> l[1]
2
>>> l[2]
3

As you might notice, the first element is in index 0. Indexes start at 0, and go up to one less than the number of items in the list. So if the list is 3 items long like this, the last item is in index 2. Trying to access index 3 gives an IndexError: list index out of range.

You can also access indices backwards starting from the end of the list, by using negative numbers.

>>> l[-1]
3
>>> l[-2]
2
>>> l[-3]
1

Here, -1 is the end of the list, and each index gives the same result as if you'd checked an index of (length of list) + (negative index number). Again, if you try to access an index that is out of range, you get an error, so the lowest index you may access is -3.

You can get the length of a sequence by using the len() function. The argument must be a sequence.

>>> len(l)
3

You can also take part of a sequence by using slice notation in the [] square brackets. Slice notation takes up to two numbers, separated by a : colon. The first number is the index to start at; the second is the index to stop before.

>>> l[0:2] # slice from 0 to just before 2
[1, 2]
>>> l[1:3] # slice from 1 to just before 3
[2, 3]
>>> l[-2:3] # slice from the second to last index (1) to just before 3
[2, 3]

In slice notation, if either number is missing, the default numbers are effectively 0 for the start point, and the length of the sequence for the end point. Thus, you can leave the missing number blank safely.

>>> l[:2] # slice until just before index 2
[1, 2]
>>> l[:1] # slice until just before index 1
[1]
>>> l[2:] # slice from 2 to the end
[3]
>>> l[-2:] # slice from the second-to-last index (1) to the end
[2, 3]
>>> l[:] # slice from the start to the end of the list; this just gives a copy!
[1, 2, 3]

You can also concatenate lists, similar to how you can strings.

>>> l = [1, 2, 3] + [4, 5, 6]
>>> l
[1, 2, 3, 4, 5, 6]

Mutability and lists

Since lists are mutable, you can alter the contents of a list, changing the value at one index for another, or adding or subtracting elements from the list.

>>> l[0] = -1
>>> l
[-1, 2, 3, 4, 5, 6]
>>> l[1:4] = [4, -6, 8]
>>> l
[-1, 4, -6, 8, 5, 6]

You can use a del statement to delete names from Python's memory. If you do this to a variable, it will delete the variable. However, if you do it to part of a list, the list will lose the elements you tell it to delete.

>>> del l[5]
>>> l
[-1, 4, -6, 8, 5]
>>> del l[2]
>>> l
[-1, 4, 8, 5]

You can also add items to the end of a list, using the "append" method. This adds the given item to the end of the list.

>>> l.append(12)
>>> l
[-1, 4, 8, 5, 12]

Alternatively, we can use the extend method, which concatenates the sequence you give it, cast to a list, and puts it at the end of the list.

>>> l.extend([13, 15, 19])
>>> l
[-1, 4, 8, 5, 12, 13, 15, 19]

Lists can contain other lists, and if you use .append on a sequence instead of .extend, you'll see this embedding effect.

>>> l = [1,2,3]
>>> l.append([4,5,6])
>>> l
[1, 2, 3, [4, 5, 6]]
>>> l[3]
[4, 5, 6]

As you can see, the fourth item in this list (at index 3) is a list, not the number 4 as you would get using .extend instead of .append. We've nested a list inside another list!

There are many other useful methods you can use on lists; a few of them are helpfully compiled at https://docs.python.org/3/tutorial/datastructures.html

Variables and Objects

While we won't get to programmer-defined objects until Lesson 11, we can still talk about objects generally. Everything that can be placed inside a variable, or as an entry in a sequence or map, is an object. The object can be thought of as a bucket. Its variable name, or its address in a sequence or map, can be thought of as a tag that's attached to the bucket.

 Bucket_L.png

l = []

When you name a new variable and set it to a blank list, you can think of it as similar to an empty bucket. Naming a new variable to another blank list is like getting out a second empty bucket.

 Two_empty_buckets.png

l = []
m = []

However, setting your new variable equal to the first variable doesn't get out a new empty bucket; you instead just stick the new variable's tag onto the old bucket!

 Bucket_L_M.png

l = []
m = l

Any changes to one of these variables, such as .append() used to add an item, will affect the other variable as well, because both are just names for the same object.

This is where copying a list using l[:] and simply assigning the new name to that list are different. Assigning a new name to the value of the old one just adds another tag, like above; taking a slice that copies the entire list makes a new copy of the list, which is independent of the original list. It has its own bucket in memory, and won't be affected by changes to the original bucket.

Copy_Between_Buckets.png 

l = [2, 3, 5]
m = l[:]

This is how all objects work in Python. Variables and other addressing methods for objects are just names; the objects don't care if they have multiple names, so you need to be careful to make a new copy of the object if you don't want to point to the old one.

Note that this only matters for mutable objects. Numbers and strings are also able to be stored in variables (tagging their "bucket"), as we've seen. However, as they are immutable, those buckets' contents can't be changed! Thus, when you assign another number or string or similar to that variable, it just changes buckets silently, and there's nothing like the above to worry about.

Tuples

Tuples, briefly, are like lists but are immutable. Instead of using [] square brackets, tuples use () parentheses. Worth noting, this means that tuples need at least one comma to be treated as tuples. (1) is just an expression that evaluates to 1, whereas (1,) is a tuple with one element that is the integer 1.

>>> t = (1, 2, 3)
>>> t[0]
1
>>> t[1:]
(2, 3)
>>> t[-2:3]
(2, 3)

Much of the same things you can do with lists, you can also do with tuples; however, you cannot add, delete or change elements in the tuple. .append, .extend, and so on do not work. This is the meaning of tuples being "immutable." The same is also true of strings; you can't assign to an index in a string, for instance.

Lists are very flexible, whereas tuples might be useful when you know exactly how long the thing is you're working with. For instance, it may make more sense to treat 2D co-ordinates as a tuple of length 2, rather than a list; there will never be more or less than 2 co-ordinates in that space, after all.

Ranges

There is technically a third type of true sequence, called a range. Ranges are created by the range function, are immutable like tuples are, and give a set of numbers across a given range. The simplest way to call the function is with one integer. This gives the set of integers from 0 to one less than that number. Thus, the range

range(10)

gives the numbers 0 through 9, which you can see if you cast it to a list.

>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

A second way to express a range is to give a start point and a stop point as the set of arguments. This will give the set of integers starting from the start argument, and going until one less than the stop argument. Thus, if the two argument range is given a first argument of 0, the second argument works the same as the first way to set a range would handle its one argument.

>>> list(range(0,10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> list(range(-2,3))
[-2, -1, 0, 1, 2]

Finally, you can add to the above range style with a third argument, which is the "step" argument. This tells the range how many steps are between each value. The step is 1 if it's left out, as you might expect. Whatever the step value is, the range will start at the start argument, and count by adding the step argument, stopping before it equals or passes the stop argument's value. If the step is negative, the numbers will be that distance apart, and the sequence will count down instead of up; the rest of the rules still apply.

>>> list(range(0,10,2))
[0, 2, 4, 6, 8]
>>> list(range(0,-10,-2))
[0, -2, -4, -6, -8]

Ranges are very useful for making for loops that go over a set number of values. Just remember that all arguments to the range function must be integers; no floating points numbers allowed.

for statements

for statements form a loop, similar to a while loop. However, instead of the loop just checking a condition over and over, for loops iterate over a sequence. You give the for loop two items, the first being the variable name you want to use for the iteration, and the second being the sequence to iterate over. The for loop will then give that variable each value in the sequence, in order from first to last element, looping over the code in its block once per iteration.

The pattern for a for statement is "for var in list" where var is the name of a variable, and list is a sequence (or an expression that evaluates to a sequence).

l = [1, 2, 3]
for i in l:
print(i)

The above code will print each element in the list on a separate line.

You can also feed a tuple or a string to the for loop; it will go through each element or character in the sequence, as for lists.

for loops can also take the break and continue statements, as normal for while loops. However, usefully, there is another thing you can do with for loops: add an else statement after the loop! Anything in the block of the else statement will be executed only if the loop exited normally, without a break statement.

# prime number finder
n = int(input("Number to test for prime: "))
# go through all possible factors
for i in range(2, n):
if n % i == 0:
print("Not prime:", n, "=", i, "*", n // i)
break
else:
# no factors found
print(n, "is prime!")

The above is adapted from the prime finding code in https://docs.python.org/3.6/tutorial/controlflow.html#break-and-continue-statements-and-else-clauses-on-loops

Note that else statements also work for while loops, and will execute when the condition becomes false and the loop exits without a break statement. However, there are far more cases one can think of where else statements are useful with for loops than with while loops.

You should now have all you need to know about loops, lists, other sequences, and also comments for good measure. Put it to good use!

Further Information

Today in the extra-info section, we have Sorting Algorithms and Game Maps.

Sorting Algorithms

The typical first sorting algorithm to be introduced to new programmers is "bubble sort." I'm going to preface this by saying bubble sort, while the simplest sort, is among the worst options available. The idea behind it is this: go through a list, starting from the beginning, checking the current number and the one next to it. If the first is less than the second, leave them alone; otherwise, swap them. Then, move over one number. When you get to the end of the list, the last item in the list is known to be the greatest number in the list, and you can now run the sort again and stop at one less than the end, and then two less, and so on until the entire list is sorted.

Bubble-sort-example-300px.gif

[Bubble sort example, Wikimedia commons]

A better algorithm is insertion sort. This starts from the beginning of a list, then checks the next number and sees where it can be inserted into the already-sorted part of the list (one item is sorted with itself, so it can skip that). If it needs to be inserted to the end, the item is not moved. The sorted part of the list is thus one longer than it was. This continues forward until the end of the list, at which point the entire list is sorted.

An abbreviated version of insertion sort can be used when inserting a new item into an already-sorted list, where this is the most efficient method for that job.

Insertion-sort-example-300px.gif

[Insertion sort example from Wikimedia commons]

Another powerful sorting algorithm is merge sort, which breaks up the list into smaller "runs" which are individually sorted in pairs, and stuck together into longer groups as the runs combine, until the entire list is sorted. Each sorted run is combined with another run by checking which item is less from the start of the lists, and inserting that into the new combined list, one by one.

Merge-sort-example-300px.gif

[Merge sort example from Wikimedia commons]

For most purposes in Python, you won't need to implement your own sorting algorithm. The sorted() built-in function uses a very powerful sorting algorithm called Timsort, which uses a combination of merge sort and insertion sort. It's quite interesting to see how it works; if you care to know more about it, look it up on your own time.

Game Maps

A typical game map is stored in a list, or in a list of lists so you can address them with, for example, gmap[x][y]. The most efficient method, however, is to just contain it in a list. In some languages this format is called an "array," and corresponds to a set of identical objects stuck one after the other in memory.

When you have a game map loaded into an array/list like this, you typically have a known width and height to the map. You can then address parts of the map with gmap[x + y * width], knowing you'll be able to address any part of the map easily.

In Python, it's impossible to address a point outside the map with this; you would get an IndexError if you tried. But what about other languages? In languages like C, which have no bounds checks on arrays, it's possible to go outside of the assigned memory for an array, and read that memory anyway. So what happens if we do this?

Strange things. Alternate "shadow" versions of the map can appear if the player is allowed to go too far to the right of such a map, ending up in a shifted version of the map. If the player goes too far in the vertical direction, meanwhile, they may rapidly find themselves in bizarre, glitched map portions that correspond to parts of memory never meant to be rendered as a map.

For an example of this behavior happening in Pokémon Red/Blue and Yellow, the first generation of the Pokémon series, see the following video.

https://www.youtube.com/watch?v=xuEsiyyYwVk

Since bounds checking is automatic in Python, this is of limited consequence to what we'll be doing in this class. However, you likely won't be only programming in Python your whole life, and eventually you'll get to poke at languages closer to the metal. Plus, it's pretty cool to see this kind of thing in action anyway, so enjoy the video!

Exercise: Programming Assignment L7

  1. Let's Sum This Again: In Lesson 5, section "Getting Numbers from Input", there is a program that takes two numbers as input from the user, and adds them together. Later, in "Errors and Exceptions", an example shows how to prevent a crash when something other than numbers gets entered.

    Using what you now know about loops, make a version of this sums program that will ask the user for input again if they enter something other than a number for either of the First or Second numbers. You will need to use while loops and exceptions.

  2. Shopping List: Make a program that repeatedly asks for "Item to buy: " and will add the item to a list, until the user enters the letter "q" by itself to quit. When the user quits, make the program spit out the shopping list as a bulleted list. An example log of what the program should look like when run is below.

    Press "q" at any time to quit.
    Item to buy: apples
    Item to buy: carrots
    Item to buy: potatoes
    Item to buy: cabbage
    Item to buy: ham
    Item to buy: q
    
    Shopping List
    * apples
    * carrots
    * potatoes
    * cabbage
    * ham
  3. Guessing Game: Time to make a simple game!

    Make a program that thinks of a random number between 1 and 100, prompts the user to enter a number in that range, and tells the user if their guess is high, low, or correct. When it's correct, congratulate the user and end the program. Also tell the user when they enter a number outside of range, or something that isn't an integer (because only whole numbers are fair).

    To generate a random integer between 1 and 100, use random.randint(1,100). You'll need to start your program file with this:

    import random
Previous
Next