As much as this series is to educate aspiring computer programmers and data scientists of all ages and all backgrounds, it is also a reminder to myself. After playing with computers and numbers for nearly 4 decades, I've also made this to keep in mind how to have fun with computers and maths.

In this fourth part, we will continue with putting in to practice what we learned in Part 1, Part 2, and Part 3 of the series.

We will go deeper into the meaning of algoritms, specifically in the context of the idea of automation. Most computer programs are ways to automate things so that humans can do human stuff instead. Or at least that's the idea.

As a reminder from Part 1, prime numbers are the numbers that all other numbers are made of. This means that any number that is only divisible by 1 or itself, is a prime number. Consequently any number that is divisible by a number other than 1 or itself, is not a prime number.

Remember the way we defined algorithm as something like a factory? Automation has to do with all those algorithms combined, as well all of their individual components. All those tiny pieces of code together make up what we refer to as automation.

We've already created simple algorithms and were left in the third version of our algo in Part 3. Before continuing to build our algo further, let's learn about generating numbers. It is a perfect example of how computers help us automated tedious processess.

Before moving on to the main section of this part, which will cover loops, another flow control method, let's become familiar with the idea and have some number generation fun.

Number generation will become handy soon, so we don't have to key in the many numbers we want to check in terms of if they are prime or not. To do this, we will use a `for`

statement. But let's first learn about `range`

, a nifty little function that comes with python.

In [21]:

```
range(10)
```

Out[21]:

At the simplest, range takes a number and creates a sequence of numbers from 0 to the input number. In this case, even though we can't see the numbers yet, we've created a sequence 0, 1, 2, 3, 4, 5, 6, 7, 8, 9.

You might wonder why we don't get a 10 but stop at 9 even though we input 10. In Python and most other programming languages counting always starts from 0. Now, let's access the numbers we're generating.

In [22]:

```
for i in range(10):
print(i)
```

Let's see what we did here. First with `for`

we basically say that we want to do something for a number of items. Then with `i`

we say that each time an item is picked, `i`

will represent it. In other words, we can use `i`

to access it inside the loop. With `range(10)`

we create a sequence of numbers from 0 through 9. As you can see, the `print(i)`

has leading spaces to it, which means that it's handled inside the loop. Note that `i`

can be called anything you like.

In [23]:

```
for number in range(10):
print(number)
```

`range`

can be used to create any sequence of integers by defining the starting and ending positions of the sequence.

In [24]:

```
for i in range(11, 18):
print(i)
```

We can also add a 'step' argument, which gives us even more control over the range of numbers we want to create. For example with step argument 2, we will get every other number in a range:

In [25]:

```
for i in range(2, 20, 2):
print(i)
```

This way we only get the even numbers between 2 and 2. Let's try the same for odd numbers.

In [26]:

```
for i in range(1, 20, 2):
print(i)
```

You've now learned a very useful and often applicable process automation; number generation. We've learn how to write any sequence of numbers, including just even or odd numbers.

There are many other ways you can use to create numbers, including random numbers, but this will be more than enough for what we want to do. Let's move on to the next section and learn about loops.

As you see, loops are just as easy and intuitive to use in Python language as everything else we've learn so far. A `for`

loop gives us a useful way to say that some process will go on *for* as long as something is true. Consider this example:

Now that you've learn yet another fundamental building block of numerical computing, we can go back to our prime number algorithm. Let's start putting what we've learn into use in our algo and also introduce one more new concept (I promise it's the last one for a while), storing something on to a variable. Storing something in to the computer memory is nothing like storing something in to human memory.

Even though we talk about storing something, it'a also nothing like storing something in to a safety deposit box where things will be kept for a long time. Computer memory is a temporary storage for something we're going to use as part of our computer program. Let's see some examples.

In [30]:

```
storage = 1
```

In [31]:

```
print(storage)
```

Basically anything in Python can be stored in a variable. It's very simple. Also, we don't have to use the `print`

to access the contents of a variable.

In [32]:

```
storage
```

Out[32]:

In the next two examples you will get a taste for how anything can be stored into a variable to access it later.

In [40]:

```
answer = 1 is 2
answer
```

Out[40]:

In [41]:

```
print_hello = print('hello')
print_hello
```

This ability to store things into variables is very powerful, particularly when we want to use the same value many times. For example, we could use this approach to simplify the most recent version of our algo. Take a note below how we are using the same operation twice:

In [42]:

```
def third_algo(left, right):
# it will not rain
if left % right is 0: # < -- here first time
return True
# it will rain a little
elif left % right is 1: # < -- here second time
return
# it will rain heavily
else:
return False
```

Now let's make a slight simplifcation by storing the operation we do twice in to memory first.

In [43]:

```
def fourth_algo(left, right):
stored_value = left % right
# it will not rain
if stored_value is 0:
return True
# it will rain a little
elif stored_value is 1:
return
# it will rain heavily
else:
return False
```

Before wrapping up for now, let's put all that we've learn together in one example.

In this following section the length of our algoritm (function) is growing. But if you look carefully, you see that the changes we make are very small in fact. Moreover, we are only making changes that you've already learn so far.

In [50]:

```
# first we create a range of numbers
numbers = range(1, 10)
# then we create a loop
for number in numbers:
# then we perform the modulus operation
result = number % number
# then we create a conditional statement for cases when it's true
if result is 0:
print(True)
# and finish with else for cases when it's false
else:
print(False)
```

Obviously we are getting True as result everytime because we are always having both the right number and the left number the same (e.g. 1 % 1, 2 % 2...).

Let's make a slight modification to take us step closer to something that will help us a great deal in finding prime numbers later. This time I'm removing the comments to keep the code neat.

In [51]:

```
left = 20
right_numbers = range(1, 20)
for right in numbers:
result = left % right
if result is 0:
print(True)
else:
print(False)
```

So what we are doing now, is fixing the left number to be 20, and then checking it against every number in the range of 1 to 20 and see if it's divisible. This makes checking if a number is prime a whole lot simpler! Let's try an example where we know it's a prime nubmer, for example 13 (it's not divisisble by any other number than 1 and itself).

In [52]:

```
left = 13 # <-- changed
right_numbers = range(1, 12) # <-- changed
for right in numbers:
result = left % right
if result is 0:
print(True)
else:
print(False)
```

Because we are starting our range from 1, one get one True in the beginning, so we have to start the range from 2 instead to get the right answer. As you can see, I changed the second line so that we scan until 12 which is the last number before 13. Let's put this inside a function as our fifth algo version and make the range start from 2 instead of 1.

In [53]:

```
def fifth_algo(left, right):
right_numbers = range(2, right) # <-- changed
for right in right_numbers: # <-- changed
result = left % right
if result is 0:
print(True)
else:
print(False)
```

Now things are starting to look good. We could now remove 'left' variable entirely as it comes as an argument from the function, and also instead of having to modify the function for the last number of the range, we also input that as an argument.

In [54]:

```
fifth_algo(7, 6)
```

That's it, we're prime number checking now! :) Because the result is False for all, we know for sure that our input, in this case 7, is a prime. There is one more very small change we can do using the skill we've already learn to make a nice improvement to what we already have. Instead of requiring the user to input the end of the range, we can automatically compute it as it's always the last number before left. In other words, it's left - 1.

In [55]:

```
def sixth_algo(left): # <-- changed
right_numbers = range(2, left - 1) # <-- changed
for right in right_numbers:
result = left % right
if result is 0:
print(True)
else:
print(False)
```

In [56]:

```
sixth_algo(9)
```

In [57]:

```
sixth_algo(11)
```

In [58]:

```
sixth_algo(19)
```

Things are working real nicely now. But clearly we will later have a problem with larger numbers with this current approach, as if we input 1,000, we will have 1,000 True or False values printed on the screen. To overcome this, we can make a small change to our latest version.

In [59]:

```
def seventh_algo(left):
right_numbers = range(2, left - 1)
output = 0 # <-- changed
for right in right_numbers:
result = left % right
if result is 0:
output += 1 # <-- changed
else:
output += 0 # <-- changed
return output # <-- changed
```

What we are doing, is first we declare a variable 'output' with starting value 0. Then instead of printing out True, we silently add 1 to output, and in case of False we add 0. Only in the end we print the value out, with the return statement that is outside of the for loop (note how it's indentation is equal to the for statement, meaning it will be processed only once the for loop has completed its job).

In [60]:

```
seventh_algo(19)
```

Out[60]:

Nice. Now we can key in much larger numbers, and just get one output.

In [61]:

```
seventh_algo(127)
```

Out[61]:

Before wrapping up, let's simplify our code slightly and instead of outputting a number, output a True or False statement. True for 'it's a prime' and False for 'it's not a prime'.

In [62]:

```
def eight_algo(left):
right_numbers = range(2, left - 1)
output = 0
for right in right_numbers:
result = left % right
if result is 0:
output += 1
return output is 0 # <-- changed
```

In [63]:

```
eight_algo(19)
```

Out[63]:

In [64]:

```
eight_algo(127)
```

Out[64]:

In [65]:

```
eight_algo(12)
```

Out[65]:

Note how we removed the else statements entirely. Because we are doing nothing in the cases where the left number is not divisible by the right number. In other words, whenever the product of the modulus operation is not zero, we do nothing. Therefore it's enough to just have the if statement without the else. This is quite common.

- Generally speaking computer programs are focused on process automation
- Loops are a highly effective method for automation
- With small changes to our code, we can make big improvements in capability
- Sometimes we can get more done with less code!
- It's very convinient to store values in to memory
- Computer memory is nothing like human memory, and also not like a safe deposit box
- Any value can be stored in to memory
- Numbers can be automatically generated with
`range`

function - It's meaningful to learn new concepts by gradually improving things

We've made great progress! Time to wrap up for now, and then in the next part we get in to the real action, looking for prime numbers! With the skills you're learn so far, you're doing a lot of the things the day-to-day of advanced programmers and data scientists is made of.