More About Looping
Earlier, you learned about for and while loops. The for loop repeats the loop body once for each element of a container. The while loop repeats the loop body continuously, testing before each execution whether some condition is true; it stops only when the condition becomes false (or a break statement is executed, which terminates any loop). In this lesson, we'll show you some other cool things you can do with loops.
Can you say 'Python is fun' 1,000 times fast? It's east in a program!
>>> for i in range(1000):
... print("Python is fun!")
...
Python is fun!
Python is fun!
[... 996 lines omitted ...]
Python is fun!
Python is fun!
>>> range(1000)
range(0, 1000)
>>> type(range(1000))
<class 'range'>
You just printed Python is fun! one thousand times, using the range() function, which generates arithmetic progressions. When we ask the interpreter to print out the result of a call to range(1000), it doesn't print out a list or a tuple, as you might expect. In fact, range() returns a special type of object known as a range object. You can iterate over this object just like you can iterate over a list [0, 1, 2, ..., 998, 999]. Using the object is different from using a list because it produces the numbers one by one as needed. This saves time and storage space that would be needed to construct a list instead.
The last example printed a string constant. In this next example, we'll print out some numbers:
>>> for i in range(4): ... print(i) ... 0 1 2 3
Did you notice that the 4 did not print? The range starts at zero, so the given end point is never part of the generated sequence. This may be confusing at first, but the interpreter has good reason for doing it like that. You'll see in this next example:
>>> names = ['John', 'Paul', 'George', 'Ringo'] >>> for i in range(3): ... print(names[i]) ... John Paul George
Generally, if you want to print only the names, you wouldn't loop over the indexes and use them to select the appropriate list elements. Instead you would loop over the list directly. If you ever see code like for i in range(len(something)), that normally indicates what's sometimes called a code smell. It's code that works–it doesn't have bugs, but weaknesses in design that might negatively impact future development. Code smells are usually an indication that something needs to be changed.
By now you can see that range() is a bit like indexing—it starts counting at zero (unless you tell it to start somewhere else) and goes on until just before it gets to the end value. What if you want a range of numbers that starts at 5 and ends at 7? You give range() two arguments instead of one:
>>> for i in range(5, 8): ... print(i) ... 5 6 7
Remember the stride we used with lists? We can do it in range() as well. Add a third argument as shown:
>>> for i in range(10, 40, 10): ... print(i) ... 10 20 30
You can also use a negative stride if you want a numerically descending sequence. In the next example, you'll see that again, the sequence stops before it actually reaches the final value:
>>> for i in range(10, -30, -10): ... print(i) ... 10 0 -10 -20
The range function is really useful and powerful. But what if you need to step through a set of numbers by tens and track which iteration you are in? For example, when counting by tens:
0 10 1 20 2 30 3 40 4 50
We can do that, right? We'll provide a counter variable and increase it with each iteration:
>>> c = 0 >>> for i in range(10, 60, 10): ... print(c, i) ... c += 1 ... 0 10 1 20 2 30 3 40 4 50
This method works, but Python gives us a better way: the function enumerate(). Like range(), it generates a sequence of values, but in this case, the values are tuples, each containing two elements. The first element is a counter that starts at zero, and the second element is the current item from the sequence that was given as an argument to enumerate(). In a for loop, you can use a tuple of two names to receive the elements, similar to the unpacking assignments we used earlier. In the example below, i is the index and e is the element from the sequence:
>>> for i, e in enumerate(range(10, 60, 10)): ... print(i, e) 0 10 1 20 2 30 3 40 4 50
| Modern Python | When you need both an index and the value while iterating,
enumerate() is the idiomatic choice. It works on any iterable,
not just ranges: for i, name in enumerate(names): is the
preferred pattern rather than for i in range(len(names)):. |
Now we'll take a look at two ways to print out a numbered list of names. There's more than one way to do it; Python has an older way of formatting, not deprecated, still works, based on the C language printf. Then it has a new "formatting mini-language" more like C#, introduced with Python 3 but also back-ported to 2.6 and above, using numbers in {curly brackets} to identify objects to format, and the .format() function we learned about in the last lesson. The format() version has more bells and whistles and makes it easier to do certain things. Also, one could argue it's cleaner in not requiring a separate operator. Here's an example using the old way:
>>> names = ['John', 'Paul', 'George']
>>> for i, name in enumerate(names):
... print('%s. %s' % (i+1, name))
...
1. John
2. Paul
3. George
Now, the same thing using the new way of formatting:
>>> for i, name in enumerate(names):
... print('{0}. {1}'.format(i+1, name))
...
1. John
2. Paul
3. George
Same results, different methods. We'll usually use the .format method in this course, but you're likely to encounter the %s method in the real world, so we'll use it occasionally.
| Note | In the above examples, we add one to the count because, although Python counts from zero, we humans normally prefer to start at one. |
| Modern Python | Both %-formatting and .format() remain valid today. A
third option—f-strings, introduced in Python 3.6—is now the most concise:
print(f'{i+1}. {name}'). F-strings embed expressions directly in
the string literal, which many find the most readable of the three styles. |
Suppose you want to print a list of all the factorials under 1000. In mathematics, N factorial is written as "N!" A factorial is calculated by multiplying successive numbers together:
1! = 1 = 1 2! = 1 x 2 = 2 3! = 1 x 2 x 3 = 6 4! = 1 x 2 x 3 x 4 = 24
The textbook definition of n factorial (as long as n is a non-negative integer) is the product of all positive integers less than or equal to n. Factorials are used in calculus, combinatorics, and probability theory.
We might use a while loop to perform the calculation instead. Create a new file in the editor window as shown:
"""Print all factorials less than 1000."""
c = 0
f = 1
while (f < 1000):
print(f)
c += 1
f = 1
for n in range(c, 0, -1):
f = f * n
Save it as factorial.py and run it. The program prints all the factorials under 1000.
1 1 2 6 24 120 720
There are actually two loops in the code. The first (or outer) loop uses the c variable to simply count upwards. The second (or inner) loop generates the factorial, based on the value of the counter.
Each iteration of the outer loop increments our counter variable c by 1, copies that to n, and resets the factorial variable f to 1. The inner loop does its work by taking the value of n, which is simply a copy of the counter, and multiplying that repeatedly against the factorial variable.
So, if you already know N!, then you can produce (N+1)! (the next value in the sequence) by multiplying N! by N+1. You can make this program even more efficient by avoiding the second loop, since the second loop would be run for each factorial. This saves a lot of work. Give it a try. Edit the code as shown:
"""Print all factorials less than 1000.""" c =01 f = 1 while (f < 1000): print(f) c += 1f = 1for n in range(c, 0, -1):f = f * nf *= c
Save and run it. The program produces the same sequence of values, but it does not repeat work unnecessarily. This becomes more important as your programs expand.
1 2 6 24 120 720
We can use the While loop when we need to validate user input. It lets us return the user back to the prompt until they provide a valid response. To implement this feature, we create an infinite loop that can only be broken by correct action by the user.
Suppose we want to force the user to provide a yes or no response. Create a new file in the editor window as shown:
"""Validate user input"""
while True:
s = input("Type 'yes' or 'no':")
if s == 'yes':
break
if s == 'no':
break
print("Wrong! Try again.")
print(s)
Save it as validate_input.py and run it. The console asks you to type yes or no. Instead, type spam and press Enter. The console responds with Wrong! Try again.. If you enter anything besides yes or no, you'll get the same response. When you finally enter yes or no, you break the While loop and your entry is printed.
The problem with this program is that it doesn't adapt itself well to more options. For example, if you need to add maybe as a possible response, that involves adding two lines of code and modifying a third. With your validate_input.py in the editor window, save it as better_validate_input.py, and edit it as shown:
"""Validate user input""" valid_inputs = ['yes', 'no', 'maybe'] input_query_string = 'Type %s: ' % ' or '.join(valid_inputs) while True:s = input("Type 'yes' or 'no':")if s == 'yes':breakif s == 'no':s = input(input_query_string) if s in valid_inputs: break print("Wrong! Try again.") print(s)
Save and run it. In this new program, the valid_inputs list variable is used to build the string that queries the user for input. It's also used to validate the user's input. So in order to add an option, you can just replace valid_inputs = ['yes', 'no', 'maybe'] with valid_inputs = ['yes', 'no', 'maybe', 'another option'].
Earlier, we learned about a useful construct for handling data called Dicts. In this next set of examples, we'll use loops to add, retrieve, and delete data. Eventually we'll combine everything into one large example to handle invitations to a party.
In the first example, you'll create a dictionary using the words of the phrase Python is awesome, using an enumerated loop to do all the hard work:
>>> data = {}
>>> for index, word in enumerate('Python is awesome'.split(' ')):
... data[index] = word
...
>>> print(data)
{0: 'Python', 1: 'is', 2: 'awesome'}
Keep this interactive session open. First you created an empty dict, then enumerated over a list containing the words split out of the "Python is awesome" string. With each execution of the loop body, you added the index of the loop as a dict key, using the word as the value of the dict element. You can do this over any list of data, from a list of words to lines of text in a file.
Our next example uses the items() method for retrieving data from a dict. items() returns a generator object, which then produces two-element tuples of keys and their corresponding values from the dict:
>>> data.items() dict_items([(0, 'Python'), (1, 'is'), (2, 'awesome')]) >>> for element in data.items(): ... print(element) ... (0, 'Python') (1, 'is') (2, 'awesome') >>> for key, value in data.items(): ... print(key, value) ... 0 Python 1 is 2 awesome
The last two commands you entered are extremely useful, because they allow you to access all the data in a dict quickly. This is a very common pattern in working with dicts in Python. The dict's items() method produces (key, value) pairs, and the for loop unpacks the tuples and binds them to key and value, respectively.
Of course, there will be times when you'll need to remove key/value pairs from a dict. Suppose you had a dict whose keys were words, and you wanted to remove all noise words (words that are not normally indexed, such as 'is' and 'at'). You can use a loop to accomplish this task:
>>> noise = ['is', 'at']
>>> data
{0: 'Python', 1: 'is', 2: 'awesome'}
>>> for key, value in data.items():
... if value in noise:
... del data[key]
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
RuntimeError: dictionary changed size during iteration
>>> data
{0: 'Python', 2: 'awesome'}
The data was deleted, but the deletion changed the size of data.items, which Python reports as an error. To avoid this problem, you need to produce a separate list rather than iterating over the dict's items (or keys) directly:
>>> data = {}
>>> for index, word in enumerate('Python is awesome'.split(' ')):
... data[index] = word
...
>>> for key, value in list(data.items()):
... if value in noise:
... del data[key]
...
>>> data
{0: 'Python', 2: 'awesome'}
We used the same techniques here that we used in earlier examples to loop through the key/values of the dictionary. And in this new example, when one key/value matched one of the listed prepositions, it deleted the element of the dict that contained that noise word. Can you think of a data structure that would have been better than a list to hold the noise words?
Good programmers build applications out of smaller code units. Our final example in this lesson will give you a chance to do just that. You'll combine the pieces of code and programming skills you've learned in this lesson to make a program that prepares a list of invitations. The program will take input as commands from the user. There are five commands: "add" to add a name to the invitation list, "delete" to remove a name, "approve" to approve an invitation that has been added, "list" to list the current invitations, and "quit" to terminate the program's operations.
Create a new file in the editor window as shown:
invites = {}
options = ['add', 'list', 'approve', 'delete', 'quit']
prompt = 'Pick an option from the list (%s): ' % ', '.join(options)
status_1 = 'unapproved'
status_2 = 'approved'
while True:
inp = input(prompt)
if inp not in options:
print('Please pick a valid option')
continue
if inp == 'add':
name = input('Enter name:')
if not name:
continue
invites[name] = status_1
elif inp == 'list':
for name, status in invites.items():
print('%s (%s)' % (name, status))
elif inp == 'approve':
for name in invites:
if invites[name] == status_1:
break
else:
print('There must be %s status invites. Please pick another option' % status_1)
continue
while True:
print('Please enter a valid name from the list below')
unapproved = []
for name in invites:
if invites[name] == status_1:
unapproved.append(name)
print(", ".join(unapproved))
name = input('Enter name:')
if not name:
break # user changed mind about approving
if name in unapproved:
invites[name] = status_2
print('%s %s' % (name, status_2))
break
elif inp == 'delete':
if not invites:
print('There must be invites before you delete any of them')
continue # user changed mind about deleting
while True:
print('Please enter a valid name from the list below')
for name, status in invites.items():
print('%s (%s)' % (name, status))
name = input('Enter name:')
if not name:
break
if name in invites:
del invites[name]
print('%s deleted' % name)
break
elif inp == 'quit':
print('Quitting invites')
print('The final invitation list follows')
for name, status in invites.items():
print('%s (%s)' % (name, status))
break
Save it as invite.py and run it. The program is really just one input validation loop that checks to make sure that the user has entered one of the five available commands. If the user has not done this, the program repeats the request for input. Most of the commands require further input, and each command allows the user to just press the Enter key to ignore the command and request another.
| Note | We used the %s method for formatting our prompts here. You should be able to change the program to use the .format() method. |
We love loops because they let us repeat the same logic again and again as necessary. This means that your program can execute some pretty complex behaviors, particularly when one loop contains others.
In the next lesson, we'll learn how programs can use and store information in files. See you there!
