Loops in Python
Choosing the Right Loop Construct
Python offers a variety of constructs to do loops. This article presents them and gives advice on their specific usage. Furthermore, we will also have a look at the performance of each looping construct in your Python code. It might be surprising for you.
Loops, Loops, Loops
A programming language typically consists of several types of basic elements, such as assignments, statements, and loops. The idea behind a loop is to repeat single actions that are stated in the body of the loop. Different kinds of loops are common:
- as long as a specified condition is true (while condition do sth.)
- until a certain condition is met (do sth. until condition)
- for a fixed number of steps (iterations) (for/from ‘x’ to ‘y’ do sth.)
- endless loop and exit/break on condition (while condition1 do sth. and exit on condition2)
Loop Constructs Supported by Python
Python supports a partial number of the constructs named above, plus it offers unique extensions to the types we have mentioned.
Basic while
Loops
while condition:
statements
As long as the “condition” is complied with all the statements in the body of the while
loop are executed at least once. After each time the statements are executed, the condition is re-evaluated. Writing a loop looks like this:
Listing 1
fruits = ["banana", "apple", "orange", "kiwi"]
position = 0
while position < len(fruits):
print(fruits[position])
position = position + 1
print("reached end of list")
This code will output one list element after the next:
banana
apple
orange
kiwi
reached end of list
while
Loops with else
Clause
This construct is specific to the Python language, but quite helpful:
while condition:
statements
else:
statements
This while
loop acts similar to the regular while
loop as introduced before. The statements in the else
part are executed as soon as the condition is no longer true. For example, in case the end of a list is reached, as in our previous example. You may interpret it as then
if the condition of the loop is no longer met.
Listing 2
fruits = ["banana", "apple", "orange", "kiwi"]
position = 0
while position < len(fruits):
print(fruits[position])
position = position + 1
else:
print("reached end of list")
This will output one list element after the next, plus the additional text from the print
statement in the else clause:
banana
apple
orange
kiwi
reached end of list
This kind of loop with an else
clause is handy in order to output messages or execute statements in case your condition fails.
One important thing to note is that the else
clause is not executed if you break
out of the while
loop or if an error is thrown from within the while
loop.
Infinite while
Loops
Infinite loops are always taught as being critical components and to be avoided if the break condition is a complicated matter. Although there are cases in which infinite loops help you to write code in an elegant way.
Here are just a few use-cases of infinite loops:
- devices that try to keep network connections active like wireless access points
- clients that try to constantly exchange data with a host system, like a network-based file system (NFS or Samba/CIFS)
- game loops for drawing and updating your game state
while True:
if condition:
break
statements
Keep in mind that the statements in the body of an endless loop are run at least once. That's why I recommend to write the break condition as the very first statement after the head of the loop. Following our example code, an infinite loop looks as follows:
Listing 3
fruits = ["banana", "apple", "orange", "kiwi"]
position = 0
while True:
if position >= len(fruits):
break
print(fruits[position])
position = position + 1
print("reached end of list")
for
Loops with an Iterator
Working with lists is described as using the keyword for
in combination with an iterator. The pseudocode looks as follows:
for temp_var in sequence:
statements
This simplifies the Python code for processing our list as follows:
Listing 4
fruits = ["banana", "apple", "orange", "kiwi"]
for food in fruits:
print(food)
print("reached end of list")
In this type of looping construct the Python interpreter handles iterating over the list and takes care that the loop does not run outside the range of the list. Keep in mind that the statements in the body of the loop are run once for every element in the list - no matter if it is just a single one, or twenty thousand.
In case the list is empty, the statements in the body of the loop are not executed. Changing the list in terms of adding or removing elements within the for
loop may confuse the Python interpreter and cause problems, so be careful.
for
Loops with Iterator and else
Clause
Similar to the while
loop, Python also offers an else
statement for the for
loop. It works similar and can be interpreted as then
, just as before. The pseudocode looks as follows:
for temp_var in sequence:
statements
else:
statements
Using this keyword our code changes as follows:
Listing 5
fruits = ["banana", "apple", "orange", "kiwi"]
for food in fruits:
print(food)
else:
print("reached end of list")
Unsupported Loop Constructs
As stated at the beginning, there are many different loop styles. However, Python does not support them all. Python does not support a do-until
loop or a foreach
loop, as possibly known from PHP. Such cases are solved using Python's in
operator that creates quite sexy code if you got familiar with it. See the alternative ways of writing a loop from above.
Which Loop to Choose?
In general the while condition
loops require a condition to be specified before the loop's statements. This may lead to the case that the statements in the body of the loop are never executed. Also, it is not always clear how many times the loop will execute for while
loops. Instead, for
loops focus on the iterator that specifies how often the statements in the body of the loop are run.
It is recommended to use a for
loop if you know exactly the number of elements to be iterated over. In contrast, a while
loop is better for when you have a boolean expression to evalutate, and not a list of elements to loop over.
Improving the Quality of your Code
Many younger programmers don't always care about the quality of their code, largely because they've grow up in a time in which nobody has to think about memory and CPU power - we just have plenty of it available in modern computers. Instead, more experienced (aka "older") developers are more prone to optimize their code as much as possible and may remember counting CPU instructions and the number of memory cells that are in use.
So what does quality mean today? In terms of effectivity it covers writing the least amount of code as possible, and effectively executing code - only as many processor instructions as needed. Firstly, with today's interpreters, run-times, and frameworks it is quite difficult to calculate that properly, and secondly it is always a trade-off between these two measures. The key questions are, how often will this code be in use and how much time shall we spent on optimizing it to win a few microseconds of CPU time.
As an example we'll have a look at a for
loop iterating over a list. Usually, we write it as follows:
Listing 6
for entry in range(0, 3):
print(entry)
This outputs the values 0, 1 and 2. The range()
method creates the iterable [0, 1, 2]
every time the head of the loop is evaluated. Therefore it is better to write it as follows:
Listing 7
entryRange = range(0, 3)
for entry in entryRange:
print(entry)
While this may not seem like much optimization for the given example, consider if the range was from 0 to 1,000,000 or more. As our list grows larger, we save more time and our code executes faster.
Furthermore, these statements can be expressed as a while
loop:
Listing 8
entryRange = range(0, 3)
index = 0
while index < len(entryRange):
print(entryRange[index])
index = index + 1
And by this point it seems a bit pointless to even use the range()
function. Instead we might as well just use a constant for the conditional and index
as a counter for the conditional and printing:
index = 0
while index < 3:
print(index)
index = index + 1
Small optimizations like these can provide small performance improvements for your loops, especially as the number of iterations become very large.
Performance Tests
So far we spoke about loop code and how to write it properly. A performance test may help to bring in some light. The idea is kindly borrowed from an interesting blog article by Ned Batchelder [1].
In use is the perf
tool that does performance tests for program code that is executed [2]. The basic call is perf stat program
whereas stat
abbreviates statistics and program is the call we would like to evaluate. To test our loop variants these calls were done:
Listing 9
perf stat python3 while-1.py
perf stat python3 while-2.py
perf stat python3 while-3.py
perf stat python3 for-4.py
perf stat python3 for-5.py
perf stat python3 for-6.py
perf stat python3 for-7.py
perf stat python3 while-8.py
This results are the average based on 10 runs due to load differences in the Linux kernel. The following table shows the results:
Topic | Listing 1 | Listing 2 | Listing 3 | Listing 4 | Listing 5 |
---|---|---|---|---|---|
task clock (msec) | 20.160077 | 18.535264 | 15.975387 | 15.427334 | 15.503672 |
context switches | 10 | 11 | 10 | 13 | 10 |
cpu migrations | 0 | 0 | 2 | 1 | 1 |
page faults | 851 | 849 | 855 | 848 | 851 |
cycles | 41,915,010 | 44,938,837 | 44,403,696 | 42,983,392 | 42,489,206 |
instructions | 46,833,820 | 46,803,187 | 46,926,383 | 46,596,667 | 46,701,350 |
For the Listings 6-8 it looks as follows:
Topic | Listing 6 | Listing 7 | Listing 8 |
---|---|---|---|
task clock (msec) | 16.480322 | 18.193437 | 15.734627 |
context switches | 9 | 11 | 11 |
cpu migrations | 0 | 0 | 1 |
page faults | 850 | 851 | 853 |
cycles | 42,424,639 | 42,569,550 | 43,038,837 |
instructions | 46,703,893 | 46,724,190 | 46,695,710 |
Conclusion
Python offers different ways to repeat actions and to write write loops. There are variants per specific use case. Our tests have shown that the loops are in the same dimension with little differences, and the optimization of the Python interpreter is quite good.
Links and References
Acknowledgements
The author would like to thank Gerold Rupprecht and Mandy Neumeyer for their support, and comments while preparing this article.