Loops (Nice)

Warning

The material in this ‘Nice’ chapter is optional. The discussion typically deals with content beyond what a novice should know. So, please finish all the ‘Need’ and ‘Good’ portions before you go through this chapter.

1 There is more to list comprehension

  1. You can have more than one loop in a list comprehension.

    [[a,b] for a in range(5) for b in ['A', 'B', 'C']]
    [[0, 'A']
     [0, 'B']
     [0, 'C']
     [1, 'A']
     [1, 'B']
     [1, 'C']
     [2, 'A']
     [2, 'B']
     [2, 'C']
     [3, 'A']
     [3, 'B']
     [3, 'C']
     [4, 'A']
     [4, 'B']
     [4, 'C']]
  2. You can even incorporate a condition!

    [[a,b] for a in range(5) for b in ['A', 'B', 'C'] if a % 2 != 0]
    [[1, 'A']
     [1, 'B']
     [1, 'C']
     [3, 'A']
     [3, 'B']
     [3, 'C']]
  3. nested_list=[[1, 2, 3], [4, 5, 6, 7]]
    [y for x in nested_list for y in x]
    nested_list=[[1, 2, 3], [4, 5, 6, 7]]
    
    output =[]
    for x in nested_list:
        for y in x:
            output.append(y)

    Here is a slightly more complicated use of list comprehension to flatten a list.

    [1, 2, 3, 4, 5, 6, 7]

    This does the same as:

2 Zipping a dictionary

zip() offers one of the easiest ways to combine two lists into a dictionary:

super_names=["Black Widow", "Iron Man", "Doctor Strange"]
real_names=["Natasha Romanoff", "Tony Stark", "Stephen Strange"]

dict(zip(real_names, super_names))
{'Natasha Romanoff': 'Black Widow', 'Tony Stark': 'Iron Man', 'Stephen Strange': 'Doctor Strange'}

In the above dict() is used to recast zip()’s output into a dictionary.

3 for and while has an else

numbers=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

for i in numbers:
    if i < 0: break
else:
    print('No negative numbers in the list')

The for and while loops in Python come with an optional else statement! The code in the else block is executed only if the loops are completed. The else code-block will not run if you exit the loop prematurely (e.g. by using break).

Shown alongside is a trivial example. You should add a negative number to the list, and re-run the snippet. There will be no message this time.

So, the else statement can be used to distinguish between the loops completing as planned or if there was a break (or return or an exception).

No negative numbers in the list

Exercises

Exercise 1 (Changing a list)  

We want a snippet of code based on a for loop to remove fruits starting with the letter ‘p’ from the following list.

fruits = ["apple", "banana", "jackfruit",
          "pineapple", "papaya", "watermelons",
          "peaches", "durian",  "mangoes",
          "strawberries", "passionfruit"
          ]

The following has been suggested as a solution. However, it does not work!

for fruit in fruits:
    if fruit[0] == "p":
        fruits.remove(fruit)

Identify, understand and fix the error.

for fruit in fruits:
    if fruit[0] == "p":
        fruits.remove(fruit)

Although the above code is elegant it has a serious flaw which you can see by using pythontutor.com to visualise the flow of the script.

A safer solution is the following:


fruits = ["apple", "banana", "jackfruit",
          "pineapple", "papaya", "watermelons",
          "peaches", "durian",  "mangoes",
          "strawberries", "passionfruit"
          ]

copy_of_fruits = fruits.copy()

# Not that we are looping(iterating) over the copy
for fruit in copy_of_fruits:
    if fruit[0] == "p":
        fruits.remove(fruit)

An even better, clean and elegant solutions is:

fruits = ["apple", "banana", "jackfruit",
          "pineapple", "papaya", "watermelons",
          "peaches", "durian",  "mangoes",
          "strawberries", "passionfruit"
          ]

[fruit for fruit in fruits if fruit[0] != "p"]

Exercise 2 (A list of powers)  

The following code is an attempt to create a list \([n, n^2,n^3]\) for several values of \(n\). We can specify the maximum value of \(n\) by changing maximum_n.

maximum_n = 5
result = [[]] * maximum_n

for n in range(1, maximum_n + 1):
    result[n - 1].append(n)
    result[n - 1].append(n**2)
    result[n - 1].append(n**3)

For maximum_n = 5 the content of result should be as shown below.

[[1, 1, 1],
 [2, 4, 8],
 [3, 9, 27],
 [4, 16, 64],
 [5, 25, 125]]

However, the code does not produce this expected result!
Identify, understand, explain and fix the bug.

An explantion

The problem arises because [[]] * 5 creates 5 references to the same empty list []. Running a list comprehension will create 5 different empty lists [].

Let’s check if what I have said is true.

maximum_n = 5

result = [[]] * maximum_n

for count, element in enumerate(result):
    print(count, id(element))
maximum_n = 5

result = [[] for _ in range(maximum_n)]

for count, element in enumerate(result):
    print(count, id(element))
0 140013001824896
1 140013001824896
2 140013001824896
3 140013001824896
4 140013001824896
0 140013001865856
1 140013001866880
2 140013001902400
3 140013001900160
4 140013001867072

A solution

maximum_n = 5
# result = [[]] * maximum_n
result = [[] for _ in range(maximum_n)]

for n in range(1, maximum_n + 1):
    result[n - 1].append(n)
    result[n - 1].append(n**2)
    result[n - 1].append(n**3)
[[1, 1, 1]
 [2, 4, 8]
 [3, 9, 27]
 [4, 16, 64]
 [5, 25, 125]]

Exercise 3 (Time profiling)  

Use %%timeit to compare the execution speeds of the following:

# Option 1 Option 2 Result
1 Creating a list of squares with for loop Creating a list of squares with while loop
2 Creating a list of squares with a for loop. Creating a list of squares with a list comprehension loop.
3 Creating a list of squares using list append() Creating a list of squares using list +=
4 Creating a list of squares using list append() Creating a list of squares using append()of Numpy
5 Creating a list of squares using Numpy Creating a list of squares using List comprehension loop.
Back to top