# 6. Loops

Loops are used to recycle logic over and over over any number of data or inputs. Without loops, for as many times as you need to apply the same logic to your data, you will have to code (or copy and paste) your logic that many times. Loops are frequently used to `iterate` over collections such as lists, sets, tuples and maps. There are two basic loops, the `while` and `for-each` loops. Let’s investigate them further below.

## 6.1. while

The while loop has the following syntax.

```1while <some-condition-is-true>:
2   # perform some logic
```

Below, we loop until the variable `n` is equal to or greater than 10. At the end of the block of code under the while loop, we increment `n` by one. If we did not increment `n` then the termination condition will never be `False` and we would loop forever.

```1n = 0
2
3while n < 10:
4    print(n)
5    n += 1
```

You can also loop endless and `break` out of a `while` loop manually. In the example below, we loop forever since we set `while True:`. However, inside the loop, we check if `n` is a multiple of 10, and if so, we issue a `break` (a manual termination) of the `while` loop.

```1n = 0
2while True:
3   print(n)
4   n += 1
5   if n % 10 == 0:
6      break
```

### 6.1.1. Exercise

Create a program to keep asking for the user to input something (anything) 5 times. Use a `while` loop to help you in this exercise. Inside the loop, simply print back out what the user entered.

Solution.

```1n = 0
2while n < 5:
3   user_input = input('enter in anything: ')
4   print(f'{user_input}')
```

### 6.1.2. Exercise

Create a program using a `while` loop to keep asking for the user to input something endlessly. Inside the loop, if the user enters in `n`, then `break`. If the user did not enter `n` as the input, then echo the user’s input back to the terminal.

Solution.

```1while True:
2   user_input = input('enter in anything: ')
3
4   if user_input == 'n':
5      break
6
7   print(f'{user_input}')
```

## 6.2. for-each

The `for-each` loop is best understood when looping over the elements of a collections. To appreciate the power of the `for-each` loop, let’s see what it would take to print a list of 10 numbers without a `for-each` loop.

``` 1numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
2
3print(numbers[0])
4print(numbers[1])
5print(numbers[2])
6print(numbers[3])
7print(numbers[4])
8print(numbers[5])
9print(numbers[6])
10print(numbers[7])
11print(numbers[8])
12print(numbers[9])
```

This approach to printing each number from a list is not scalable if there are a million numbers to print. We need to use the `for-each` loop. The syntax of the `for-each` loop is as follows.

```1for <element> in <collection>:
2   # perform some logic over the element
```

### 6.2.1. Basic for-each

Here are some examples of using a `for-each` loop against different types of collections.

``` 1# looping over an array/list
2number_list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
3
4for number in number_list:
5    print(number)
6
7# looping over a set
8number_set = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
9
10for number in number_set:
11    print(number)
12
13# looping over a tuple
14number_tuple = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
15
16for number in number_tuple:
17    print(number)
18
19# looping over a map
20number_map = {
21    'a': 0,
22    'b': 1,
23    'c': 2
24}
25
26for key, val in number_map.items():
27    print(key, val)
```

### 6.2.2. Exercise

Loop through each number in the list below and print the number and what the number times 2 would be.

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

Solution.

```1numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2
3for number in numbers:
4   n = number * 2
5   print(f'{number} x 2 = {n}')
```

### 6.2.3. Exercise

Loop through each number in the list below and print the number and what the number times 2 would be only if the number is even.

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

Solution.

```1numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2
3for number in numbers:
4   if number % 2 == 0:
5      n = number * 2
6      print(f'{number} x 2 = {n}')
```

### 6.2.4. Exercise

Loop through each number in the list below and print the number and what the number times 3 would be only if the number times 3 is odd.

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

Solution.

```1numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2
3for number in numbers:
4   n = number * 3
5   if n % 2 != 0:
6      print(f'{number} x 3 = {n}')
```

### 6.2.5. Exercise

Loop through each key-value pair in the following dictionary. Since the values are tuples, loop through each element in the tuple and print them.

```1data = {
2   'fred': (28, 150.5, 5.5),
3   'john': (32, 180.2, 6.2)
4}
```

Solution.

```1data = {
2   'fred': (28, 150.5, 5.5),
3   'john': (32, 180.2, 6.2)
4}
5
6for key, tup in data.items():
7   for item in tup:
8      print(item)
```

### 6.2.6. for-each with break

Breaking is also possible inside a for-each loop.

```1numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2
3for number in numbers:
4    if number == 3:
5        print('found 3')
6        break
```

### 6.2.7. Exercise

Loop through the following list and break after the third odd number is encountered.

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

Solution.

```1numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2
3total_odds = 0
4
5for number in numbers:
6   if number % 2 != 0:
7      total_odds += 1
8   if total_odds == 3:
9      break
```

### 6.2.8. for-each with continue

When used inside a `for-each` or `while` loop, the `continue` command forces the logic back to the start of the code block inside the loop; all code below the `continue` are skipped. The code below iterates through each integer a list and skips printing the integer if it is odd.

```1numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2
3for number in numbers:
4    if number % 2 != 0:
5        continue
6    print(number)
7
```

### 6.2.9. Exercise

Loop through the following list of names and skip printing names that do not start with a `j`. Use the `continue` command to achieve this request.

```1names = ['jane', 'mary', 'josephine', 'nancy']
```

Solution.

```1names = ['jane', 'mary', 'josephine', 'nancy']
2
3for name in names:
4   if not name.startswith('j'):
5      continue
6   print(name)
```

### 6.2.10. for-each with enumeration

Sometimes, we want to access the corresponding index of an element as we are iterating through a collection. To get the index and element, we can pass the collection to the `enumerate()` function.

```1numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2
3for i, number in enumerate(numbers):
4    print(i, number)
```

Note that `enumerate()` will return a tuple, and so we can loop over the elements as follows.

```1numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2
3for tup in enumerate(numbers):
4   print(tup[0], tup[1])
```

Or, we can unpack the tuple inside the `for-each` loop.

```1numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2
3for tup in enumerate(numbers):
4   i, number = tup
5   print(i, number)
```

Typically, we prefer to unpack the tuple with the `for-each` loop as `for i, numbers in enumerate(numbers):`.

If we did not want to start the index at 0, then we can specify a starting index as follows.

``` 1numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2
3for i, number in enumerate(numbers, 10):
4   print(i, number)
5
6# the code above will print
7# 10 0
8# 11 1
9# 12 2
10# and so on ...
```

### 6.2.11. Exercise

Loop through the following list of names and print the name only if the name starts with a `j` and the associated index is odd.

```1names = ['jane', 'mary', 'josephine', 'jack', 'nancy']
```

Solution.

```1names = ['jane', 'mary', 'josephine', 'jack', 'nancy']
2
3for i, name in enumerate(names):
4   if name.startswith('j') and i % 2 != 0:
5      print(name)
```

### 6.2.12. Looping over two lists

What if we want to iterate over 2 collections at the same time? We can use the `zip()` function. `zip()` aligns the elements from 2 or more lists and creates a tuple for each aligned elements. We can create a list from zipping two lists as follows.

```1names = ['Jack', 'John', 'Joe']
2ages = [18, 19, 20]
3
4persons = list(zip(names, ages))
5print(persons)
6
7# should print [('Jack', 18), ('John', 19), ('Joe', 20)]
```

The example below show the use of using a `for-each` loop with `zip()`. Note how we unpack the tuple elements generated by `zip()`.

```1names = ['Jack', 'John', 'Joe']
2ages = [18, 19, 20]
3
4for name, age in zip(names, ages):
5    print(name, age)
```

### 6.2.13. Exercise

Loop over the two lists below simultaneously using `zip()` and print the name and age only if the age is even.

```1names = ['Jack', 'John', 'Joe']
2ages = [18, 19, 20]
```

Solution.

```1names = ['Jack', 'John', 'Joe']
2ages = [18, 19, 20]
3
4for name, age in zip(names, ages):
5   if age % 2 == 0:
6      print(f'{name}, {age}')
```

### 6.2.14. Looping with enumerate and zip

We can combine `enumerate()` with `zip()` as follows.

```1names = ['Jack', 'John', 'Joe']
2ages = [18, 19, 20]
3persons = list(enumerate(zip(names, ages)))
4print(persons)
5
6# the above should print [(0, ('Jack', 18)), (1, ('John', 19)), (2, ('Joe', 20))]
7# note the result is a list of tuples, where each tuple has the index in the first
8# position and a tuple in the second position
```

Here is how we can use `enumerate()` with `zip()` with a `for-each` loop.

```1names = ['Jack', 'John', 'Joe']
2ages = [18, 19, 20]
3
4for i, (name, age) in enumerate(zip(names, ages)):
5    print(i, name, age)
```

### 6.2.15. Exercise

Loop through both lists below. Only print the name and age if the name starts with a `J`, the index is odd and the age is odd.

```1names = ['Mary', 'Jack', 'John', 'Nancy', 'Sam', 'Jeremy', 'Mark']
2ages = [18, 19, 21, 24, 26, 27, 32]
```
```1names = ['Mary', 'Jack', 'John', 'Nancy', 'Sam', 'Jeremy', 'Mark']
2ages = [18, 19, 21, 24, 26, 27, 32]
3
4for i, (name, age) in enumerate(zip(names, ages)):
5   if i % 2 != 0 and age % 2 != 0 and name.startswith('J'):
6      print(f'{i}, {name}, {age}')
```

## 6.3. Comprehension

Comprehensions are a way to transform an existing collection into a new one using the `for-each` loop. There are comprehensions to generate lists, sets and dictionaries.

### 6.3.1. List comprehension

The `list comprehension` has the following syntax: `[<expression> for <element> in <collection> <filter:optional>]`. The `<expression>` is required and the `<filter:optional>` is optional. The `<expression>` is typically a transformation of the `<element>`. Let’s say we have a list `numbers = [1, 2, 3, 4]` and we want to transform this list into a new one where each element is multiplied by two. We can use a regular `for-each` loop or a list comprehension.

```1# do not create a list this way
2
3numbers = []
4for num in [1, 2, 3, 4]:
5    n = num * 2
6    numbers.append(n)
7
8# use a list comprehension
9numbers = [num * 2 for num in [1, 2, 3, 4]]
```

An example of using a filter is as follows. In this example, we transform each integer to a new number by multiplying it by 3 and we filter for for only odd-indexed numbers.

```1numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2nums = [number * 3 for i, number in numbers if i % 2 != 0]
```

### 6.3.2. Exercise

Transform the list below into a new one with all boolean values, where each element in the new list indicates if the corresponding element in the existing list is even `True` or odd `False`.

```1numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
```
```1numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2nums = [True if number % 2 == 0 else False for number in numbers]
```

### 6.3.3. Nested list comprehension

List comprehensions can also be nested as in the example below. Note that we have a list of lists, which represents a two-dimensional matrix. So we have to iterate over the rows and then the columns in that row. The result is that we end up with a `flattened` list (the new list is not like the first list, which is a list of lists).

```1matrix = [[1, 2], [3, 4], [5, 6]]
2
3flattened = [col for row in matrix for col in row]
4
5# flattened = [1, 2, 3, 4, 5, 6]
```

### 6.3.4. Exercise

Flatten the matrix below using a nested list comprehension, but, only include those elements whose row and column indices are equal.

```1matrix = [[1, 2, 3], [3, 4, 5], [5, 6, 7]]
```

Solution.

```1matrix = [[1, 2, 3], [3, 4, 5], [5, 6, 7]]
2diagonal = [col for i, row in enumerate(matrix) for j, col in enumerate(row) if i == j]
```

### 6.3.5. Set comprehension

`Set comprehensions` are just like list comprensions, except, instead of using `[]` to setup the comprehension, we use `{}`. The `set comprehension` has the following syntax: `{<expression> for <element> in <collection> <filter:optional>}`.

```1# do not create a set in this way
2
3s = set()
4for name in ['Jack', 'John', 'Joe', 'Mary']:
5    n = len(name)
7
8# create a set in this way, using a set comprehension
9s = {len(name) for name in ['Jack', 'John', 'Joe', 'Mary']}
```

### 6.3.6. Exercise

Find all the unique odd numbers in the following list of numbers.

```1numbers = [1, 2, 3, 4, 5, 2, 3, 5, 1, 1]
```

Solution.

```1numbers = [1, 2, 3, 4, 5, 2, 3, 5, 1, 1]
2nums = {number for number in numbers if number % 2 != 0}
```

### 6.3.7. Dictionary comprehension

A `dictionary comprehension` is used to generate a new dictionary. The `dictionary comprehension` has the following syntax: `{<key>: <expression> for <element> in <collection> <filter:optional>}`.

``` 1# do not create a map in this way
2names = ['Jack', 'John', 'Joe', 'Mary']
3
4m = {}
5for name in names:
6    m[name] = len(name)
7
8
9# create a map in this way, using a map comprehension
10names = ['Jack', 'John', 'Joe', 'Mary']
11
12m = {name: len(name) for name in names}
```

### 6.3.8. Exercise

Below is a list of names. Note that the elements are tuples, where the first element in the tuple is the first name and the second element is the last name. Transform the list into a dictionary where they key is the initials of each name and the corresponding value is the full name.

```1names = [('Jack', 'Washington'), ('John', 'Doe'), ('Joe', 'Black'), ('Mary', 'Swanson')]
```

Solution.

```1names = [('Jack', 'Washington'), ('John', 'Doe'), ('Joe', 'Black'), ('Mary', 'Swanson')]
2persons = {f'{fname[0]}.{lname[0]}.': f'{fname} {lname}' for fname, lname in names}
3print(persons)
4
5# should print {'J.W.': 'Jack Washington', 'J.D.': 'John Doe', 'J.B.': 'Joe Black', 'M.S.': 'Mary Swanson'}
```

### 6.3.9. Generator comprehension

With list, set and dictionary comprehensions, the resulting data is evaluated and created immediately. The `generator comprehension`, however, only evaluates the expression as needed. The former comprehensions are `eagerly` evaluated and the latter comprehension is `lazily` evaluated. If we need to generate a new collection that does not need to be stored, use a generator comprehension as intermediary results will not be stored. The syntax for a generator comprehension is nearly identical to the list comprehension, except that we use `()` instead of `[]`: `(<expression> for <element> in <collection> <filter:optional>)`

```1# do not do this, it's not memory efficient
2x = (10, ) * 100000
3y = [num * 2 for num in x]
4
5# do this instead, use a generator comprehension
6x = (10, ) * 100000
7y = (num * 2 for num in x)
```

### 6.3.10. Exercise

Write a program to ask the user to input a number over and over (until the user requests to quit by entering `q` to quit). Each time a user enters a number, whatever that number is, compute the list of numbers that results when multiplying it by 1, 2, 3, 4, 5, 6, 7, 8, 9, 10.

Solution.

``` 1# we use a tuple for the collection
2# why? because we do not want this collection to ever be modified
3# we define this collection of numbers outside the while loop
4# why? because we only need to define it once, not over and over inside the loop
5multipliers = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
6
7while True:
8   user_input = input('enter a number or "q" to quit: ')
9
10   if user_input == 'q':
11      break
12
13   num = int(user_input)
14   numbers = [n * num for n in multipliers]
15   print(numbers)
```