 Welcome back to chapter 4 of the Introduction to Python Programming Course. This chapter is on recursion and looping. Recursion and looping are two sides of the same coin, and the coin is called iteration. So in this chapter we will talk about various ways with which we can make some code run repetitively in our programs. We start by recursion. Recursion is commonly regarded a more advanced topic that is usually taught towards the end of a programming course. So the question is why did I put it this early in the course? Well, I realized over the last couple of times I taught this course that the less a student knows, the less the student will be confused when recursion is introduced. So let's try it and I'm pretty sure with the examples in this chapter, recursion can be understood by anyone. Okay, so what is recursion? There is a joke among programmers and computer scientists that goes like this. In order to understand recursion, you must first understand recursion. And this joke already gets to the point of it. Recursion means we are defining something with a reference to itself. Now, back in high school and elementary school, when we were taught how to define a term with some other words, we were taught not to use the term that we are defining in the definition. Because otherwise, if a definition is self-referential, we cannot really use it for anything really. However, as programmers, we can do so and we certainly will. And we will see that this is actually quite a nice approach to implement an iteration logic. So let's start with a very easy example. Imagine you are at a party at a New Year's Eve party and it's 10 seconds before the New Year starts. What usually happens is that someone starts a countdown and then the people start to count down from 10, 9 and so on. And when we hit 0, then the people all scream Happy New Year. Okay, so how would you implement code that does that? And also, how would we parameterize such a code so that we can actually set the number from which the countdown starts? So see here a countdown function that we define and it takes one parameter called N and N is supposed to be an integer and it's the seconds until the party begins. So what happens if, for example, I want to countdown from 3? So the function will be called with N set to 3. What then happens is in the first line of code, we check if N is equal to 0 and of course it is not. So we go to the else case and then we print out the number. So we print out 3. And then what happens is the function calls itself. And before doing so, it decrements N by 1. So it calls countdown with an N set to 1. And then the same thing happens again and of course N is still not 0. So we hit the else branch again and we print out 2 and we call the function one more time and then N is 1. With N being 1, the if branch is still not reached because it's not 0. So we go into the else branch one more time. We print out 1 and then the function will call itself one more time now with N being 0. And then finally, when the function is called with N being 0, then what happens? We go into the if branch, we print happy new year and then what happens? Well, nothing happens anymore. Once we hit the print happy new year line. This is the last line of code that will actually be executed because the if statement of course continues afterwards. But we see there is no code after the if statement is over. So what happens then? The function countdown when called with N set to 0 will return. And if we return the value none, the special value that is always returned if we don't provide any explicit return value. And then we see on the screen three, two, one and happy new year. Okay, so I defined the function and now let's see if the function works. So I call the function with an argument of three and I execute it and indeed I do see three, two, one and happy new year. So while this is rather, I would say trivial, let's look at what is going on in the memory. So remember Python tutor. So here I have four tabs with the examples from this chapter. And I copy pasted the code of some of the examples in a chapter into Python tutor so that we can now go over the function call in steps. So in the first step I execute I click next. What happens is the function is defined. We see that in memory as there is a function object created in the object part of the memory. And then in the names part of the memory we have a global name called countdown which references the function object. So far the function has not been called. We only defined it. Now in the second step we call countdown with an argument of three. And now what happens? A function gets called. And as we remember from chapter two, when the function is called it gets its own space where it keeps track of the variable names inside it as it runs. So we see here n is set to three. And now what happens is exactly what I just told you what would happen. We go into the elf in the elf branch because the condition of the if branch is not yet fulfilled. We print out n and then we call countdown again with n being two. So now let me click further. So we go we see we hit the hit if the if line it will skip. We skip. We go into the elf branch and now print n is executed. We see the printed output up here. So we see a three. And then what happens is the last line of code in the function will be executed countdown n minus one. And what happens is this is of course a function call another function call. So what happens is for the second function call another space in memory in the names area of the memory will be reserved for the second function call. And within that n is set to two. So note the important observation here is that the first function call is still there. It has still the function has not yet returned. In other words, we have two instances of the same function being run at the same time at the moment. Of course, both functions are at a different stage. So the first function call is at its last line. It's actually waiting for the second function call to return so that the first function call can continue. So now let's go on. The second function call is being executed. We hit the else branch again. We print out two and then yet another function call with n being one is initiated, which leads to another area in our memory where n is set to one. And remember that the other two function calls, they just don't go away yet because they are still waiting. So the second function call is now waiting for the third function call to finish. And the first function call is waiting for the second function call to finish. So we have a dependency here, right? The first one is waiting for the second. The second one is waiting for the third and so on. And we do that one more time. So we go into the else branch. We print out one and then the countdown function calls itself with n being set to zero. So what happens now? We go into the if branch and then print happy new year. The line will be executed, right? So we will see happy new year in the output. And now the fourth function call that is currently going on where n is zero is basically just in the moment before it is finished. And we see that because we see this return value here written. And the return value will be none and it is none because we didn't specify any explicit return value. So if I click next now, this function call will now disappear because why? Well, the print happy new year line is the last line that is basically executed. And below that there is no further line of code in the body of the function. So if I hit next, the scope will go away. And now we go back. So Python returns its flow of execution to the countdown function where n was set to one. And now what does this function call do? Well, it is already in its last line. And the only thing that this function call is doing, it was waiting for the other function call to finish. And now that the other function call is finished, now this function call is also finished because it's also the last line of code. So now the next scope will go away. And so will the other two scopes. And finally, after all the function calls are gone, we are left with an output three to one happy new year. And we are left with only one variable name called countdown, which references the function object. So all the names are now gone. They were local scope. And as we learned in chapter two, local scope goes away after a function call. By the way, this function is a function that does not have a return value, but it has a side effect. And the side effect is that we print something to the screen. So otherwise, this function would be pointless because without a return value, why would we ever call a function if there is no output? So the output in this case is a side effect. Okay, that is recursion. So let's reiterate here. What is recursion? Recursion means to go back to the simple example, we have a function that consists of some if else logic or some, let's say, conditional logic. And in some cases, the function calls itself. In some cases, it doesn't. And this is the structure of a recursion. So recursion is kind of like a recursive definition, something that is defined with, you know, by referring to itself. However, there has to be a way out at the end. If there is no way out, then if there were, for example, a countdown call in the other branch in the if branch here as well, then this function would keep on calling itself and never stop. And that would be bad. That's not what we want. And we will see an example of infinite recursion later on. So to summarize, a recursion means we have a function which has a conditional logic in it. In some cases, the function calls itself with a modified argument. Otherwise, you know, we wouldn't get to an end. And then we have a case, at least one case where the function does not call itself. We call any case where the function does not call itself a base case. So that's why I write here base case. Okay. That's recursion in a trivial example. Let's look at some other examples. My assumption is that all of you know recursion already. And that is why I think I should teach recursion early in the programming course. Why do you know recursion already? Well, you have seen examples in math when you went to high school where recursion was taught. However, most likely the teacher did not call it recursion back then. So I give you an example. The factorial of a number. So you may remember that the factorial of a number is a concept that is taught usually within the scope of probability theory or statistics. So whenever we want to estimate what is the probability of some event happening, we divide the number of, you know, all the cases where the event happens by the number of all the possible events. And oftentimes when we have to figure out how many possibilities are there to throw, I don't know, a coin or how many possibilities are there to throw, let's say, six dice or something like that. Usually a factorial is involved. So what is a factorial? You remember this definition? The factorial is written as an exclamation mark. So n factorial here, n factorial is the number where it's basically the number that gets multiplied by n minus one factorial. So you see here already a recursive definition. I give you an example. So what is three factorial? Three factorial is just three times two times one. So this would be six. So the factorial in other words in maybe easier terms is the multiplication of all the numbers, of all the whole numbers starting from the number for which you want to calculate factorial down to one in decrements of one. So again, factorial of three is just three times two times one, which is six. And of course, because we don't want, you know, to have only calculated the factorial for one particular number, but in a general case, we have to formulate the factorial in a generic way. And we do so with this formula. We say n factorial is n times n minus one factorial. And then the mathematicians teach you that zero factorial is defined to be one. Back in high school, I always wondered, why is this the case? Is this a natural law? Well, at the end of the day, this is just a definition. So this is something that mathematics people define to be. So they didn't derive it from somewhere. But this definition makes sense. So what can we see here in this definition of the factorial? Well, we can see that, again, we have a recursive definition. So n factorial is defined in terms of a factorial of another factorial, namely the factorial n minus one. And we have a base case. So we have a case where the factorial is defined without referencing another factorial. And this is zero factorial, which is defined to be one. So base case and the non-base case, also called the recursive case. So let's see how can we translate this simple example from mathematics that you all know into Python code. So here's a function factorial, which takes one parameter n. n is an integer. So we write that in the doc string, of course. And the number returned is also an integer. It's most likely going to be a very big integer because the factorial is a very fast-growing function. And then the code begins. And we basically copy-paste the math into code. So here we have the base case. So whenever n is zero, which is here, we compare n to zero. We just return one because that's the case by definition. And that is the base case because we don't have to call factorial again. And then in the other case, in the other alternative, what we do is we call the factorial function again. And we decrement the parameter n by one. And whatever we get back, we store in a variable called recurs. And then the actual result that we are interested in is just n times whatever we get back from the factorial function from the recursive call here. This is the result. And then, of course, we return the result. So what is different here in this example compared to the previous example? Well, here, obviously, we have a return value. And this is usually what you want and what you see because usually whenever you use a recursion to calculate something, you are interested in some solution to something. You are not interested in a side effect of, let's say, printing out a countdown, but you are interested in some solution to a problem, in this case, a math problem. So we have to return some result from the function. And other than that, not so much is different here. Now, one difficulty many people in the beginning have a problem by following basically the flow of execution here. So given some n that is not 0, that is greater than 0, the function runs and it will end up in the else branch. And then the beginner, the beginning student, so to say, reads this line of code. And then what do you do? Do you follow back up again, the factorial n minus 1, and you go back up and start doing your analysis by reading from top to bottom again? The answer is no. When I see such code, what I do is when I see a recursive call, I just assume that I get back a correct result for the moment when I read this. And then I assume the recursive result is correct and then I just go on with my computation. So when I read this code, I just read this line of code and then I continue to read this line of code. I don't go back and read the code from top to bottom again because this is, of course, what would happen if I called this function. When I call this function, of course, the flow of execution goes back up. But when I read this code, I don't want to follow the flow of execution. So I just assume that I get back a good return value from this call here. Okay, so let's define this function. Let's see if it works. And then we will look in Python Tutor and what are the underlying details in memory. And then as we look into this function in memory, we will also understand how these functions work when they are being executed. So factorial of three, this would be just three times two times one, which is six. And the function indeed gives me back six. Okay, so let's go to Python Tutor again and see here I copy pasted the code of the factorial function here. So I execute the first step and the function is defined. I have a variable factorial in the global namespace, which references the function object. And then in the second step, I call the function with an argument of three and I want to store the result in a variable called solution. So when I call factorial of three, what happens is, for the new function call, a new area in memory is created where n is set to three and this n is only valid as this function is being executed. And then we go from top to bottom and because n is three, we end up in the else branch. And then we read this line of code, recurse equals factorial of n minus one. And when we hit this line of code, what's going to happen, the right-hand side of course is evaluated first. Before the result of the right-hand side can be assigned to the variable on the left-hand side, which is why once we start executing this line of code, which has the red arrow here, what happens is immediately I will get another area in memory where now n is being set to two. And this is the second function that is being run at the same time. So now the first function call is waiting for the second function call to finish. And then within the second function call, the code starts to run from top to bottom again and because n is two, we end up at the very same line of code again and we make another function call and set n to n now. So two minus one is one. So we have another function call simultaneously going on where now n is set to one. And now the second function call is waiting for the third one to finish. So again, just we had in the countdown example before, every function call is now waiting on the subsequent function call to finish. So what happens if factorial of one is executed? Well, we hit the else branch one more time and now the function is called with n being set to zero. And so now the third function here is waiting for the fourth to finish and now n is zero. So what happens is I hit the return statement. I hit return one and we learned in chapter two that whenever we hit a return statement, the function is done. It stops. Whatever comes after a return statement will never be executed in this function call because the return statement finishes the function call and returns whatever value is being evaluated on the right side next to the return statement. And it's just one here. So what happens here? When I click next, the factorial function where n is zero is now in the instant where it's basically being finished up and we see that the return value is going to be one. So previously the return value was none because we didn't return anything. Now the return value is one. It's now being returned to where? Well, to the function call where n is one. So this function call where n was set to one basically called or created this other function call where n is no zero. So once this function call is done, the return value is given back to the third function call here or the third function call to factorial. And now what does this function call do? It becomes active again. That's what the blue coloring means. That's the active area in management. And now what happens is factorial of n minus one has now returned the number one. So now at the right-hand side here of this assignment statement is now one because that's what we got back. And now we store the one in the variable recurs. And then we go on and then we say result is n times recurs and n is one and recurs is also one. So result will be one times one, which is also one. So then we actually return one again. So we see result is also one. And now the return value because that we have return result. So the return value is also one. And this one is now given back to the next function call above where n was set to two. And now what does this function call do? Well, it continues. This function call was waiting now for the other function call to finish. Now that the other function call to finish, it continues. It comes back to life. And the right-hand side of this line of code is now done being evaluated. And this will now be one because the one was previously returned. So recurs is also here now one. And then the result will now be n times recurs. So it will be two times one, which means we get a result of two. And the two will then also become the return value. And this two will be given up to the function that's called this function. And then one more time, the factorial n minus one call here returns. And now this has been evaluated to two. So recurs is now set to two. And then the result is n times recurs, which is three times two, which would be six. That is why result will become six. And then I return the result, I return six. And that's the return value now. And now the function is gone. And the solution is now set to six. OK. So we see already in this easy example here that a lot is going on in memory. And I use Python Tutor here because drawing this in my memory diagram is rather tedious because I have to, you know, I need lots of space in my memory diagram just to draw four function calls. But I think you get the point. And I think Python Tutor is a good visualization in this case for you to understand what is going on in this function. OK. Let's go on in the presentation. So how can we improve the factorial function? So you remember that in this course I am also trying to teach to you some of the best practices that you should follow. So let's maybe go back to the version that we were looking at. So what comes to mind when we read this code? Well, when we read this code, we have two branches, obviously, and in both branches we have a return. And I told you the rule whenever we hit the return statement, the function call is over. So whenever, that means whenever we hit the if branch and go to the return one, everything that is below the return one, which includes the else here, so everything that I mark here can never be executed, right? Once I hit the return one, everything that is below can never be executed. So in other words, I don't have to use this else here. I don't need this. Instead of this else, I can just get rid of this else. And then, of course, I have to detent now. I have to detent the code block. And now this code is exactly the same as before. So again, this is the original if else branch. And if I get rid of the else and detent the code block here, this code is exactly the same as before. It does exactly the same. Of course, it looks a little bit different. I'm saving one word, the else word, but the code does exactly the same. And why again? Because whenever I go into the if branch, and whenever I go into the if branch here, I will hit the return statement, and whenever I hit the return statement, the function call is over, and whatever happens below will never be executed in this particular function call. So because of that, I do that. And whenever I can do that, I should do that. And there is a name for writing code in this way. This is the so-called early exit pattern. And we call this early exit pattern because when we hit this return, we exit from the function early, early mean before the end. And yeah, that's just one way to make code look a little bit nicer. Why is this code nicer? Because now I'm conveying another idea as the programmer. I'm conveying the idea that whenever I hit this return one, the function is over. So I know, in other words, when I'm down here, I cannot have been, I must not be here in the first place. So in other words, when I read this code top to bottom, I know that, okay, now this part is done. I don't have to worry about this condition again when reading the rest of the function call here, or the function definition. So it's just a little bit nicer. And you will see the idea of the early exit pattern also in some other contexts. And it saves us a little bit of time. It makes the code a little bit nicer. And then, of course, going from this formulation here to the one that I'm about to show you is also not so hard. So maybe I do it interactively. So maybe we keep staying here. And what we see here is that the factorial n minus one will be stored in the recursive variable. And then I just use the recursive variable in the next line. So how about this? How about I just go ahead and take this factorial n minus one and I copy paste it to wherever the word recurs was standing and I just get rid of this here and I have saved even more lines of code here. So this code is absolutely equivalent. It's actually a bit cheaper because we don't have to use a variable. So we don't have to set and read one variable in between. So it's a little bit faster, but this is really bogus actually. So you could, in Python, as we learned, setting and reading variables is very cheap because all variables really are, it's just references and that is a very cheap operation. And then, of course, we can do the same here. I calculate here the result. I store it in result and in the next line I return result. So how about this? I just copy paste this code here and get rid of this line and maybe I also get rid of the space of the empty line in between and now I have a very compact version of the code. And the funny thing is, if you look above at the mathematical definition, it basically looks the same. So in other words, this is code that gets as close as possible to the actual formulation in math and that's one of the reasons by many people like Python because Python is a language that allows for formulating code in a way that looks like math. Okay. And if I now go ahead, this is exactly what you will find here. That's the second version. I will define the function and then, of course, factorial of three is still six so it still works. Okay. Let's look at another example. So the trivial example, the countdown was very simple and it was basically just to illustrate the point so that's why it was that simple and also I think the factorial example is rather trivial. So I know that if you begin with recursion, it may be hard to understand the first time around, but once you understood recursion and I think you all have, then this shouldn't be too hard. But now the question is, is recursion always this intuitive? So is it always similar to something we can express in math and the answer to that is unfortunately no. There are examples where an algorithm is recursive, but we cannot really intuitively tell what the algorithm does. And one such example is the so-called Euclid's algorithm. Euclid is an old Greek mathematician and philosopher and he back then in, I think, two or three hundred before Christ developed an algorithm that can be used to determine the greatest common divisor of two integers. So how did he do that? He saw this. He saw that if I'm given two integers a and b and I want to calculate the greatest common divisor, all I need to do is, I need to check if one of them, in this case b is zero, then I just return a and if this is not the case, so this is the base case and then the other case down here is the recursive case. We see that it's the recursive case because the function calls itself and then he says the recursive case is just we switch a and b, so where we had a before, we now put the b and where we had b before, we say a modulo divide by b and this is basically what he came up with and if you ask me, if I look at this, I don't find this intuitive. There are ways to visualize this algorithm so that you can intuitively understand what it does, but I don't think that this is an intuitive algorithm. However, it works and it also has another nice side benefit. It's a very efficient algorithm. So let's look at it. How do we check an algorithm that calculates greatest common divisors? So we just generate some test cases, for example, 12 and 4, so we know that for those two numbers the greatest common divisor should be 4. So it is, so it seems to work and then this is what I meant with the algorithm is extremely efficient, so I just take two very big numbers and I try to calculate the greatest common divisor and it also works and it seems to be 9, but for the second example here, we cannot really manually verify this. I couldn't do this in my head for 12 and 4 we could and then since we're here testing functions, I told you before that or at least in the exercises you will see that when you write functions and you want to test at least for plausibility if your function is correct, you should make sure that you also test so-called edge cases and what is an edge case when testing two integer numbers for their greatest common divisor? Well, an edge case would be to just use two prime numbers because we know from high school that a prime number is any number above one that is only divisible by one and itself. So any two prime numbers they won't have a great common divisor or in other words the greatest common divisor of any two prime numbers so let's look at the example so I chose 7 which we know is a prime number and 7,919 7,919 is the one thousandth prime number so that's why I chose it I looked it up in a table you don't know this by heart of course and if you check it, we get back one and that means the algorithm seems to be good at least it works in edge cases okay, let's look at some other example which is a typical example of a recursion the so-called Fibonacci numbers and also as a precursor I will already say that whenever you will solve Fibonacci numbers you will not use a recursion in practice so Fibonacci numbers are efficiently solved with another approach and we will see this other approach at the end of this lecture so here I will show you a recursive version of Fibonacci numbers for illustration purposes to illustrate some other points that we have not seen before and also to expose you to the idea of exponential growth so what are Fibonacci numbers? so Fibonacci numbers I will give you the example here on the paper so we define the first Fibonacci number as 0 and then we define the second Fibonacci number as 1 in some other versions you will see that the first Fibonacci number is 1 and the second Fibonacci number is 2 however you can also start with 0 and 1 now the important thing is those two numbers are Fibonacci numbers by definition so this is not a natural law this is just what we define and then we have one rule as to how to calculate Fibonacci numbers and this will be the next number in line will always be the sum of the previous two so 0 plus 1 is 1 and then we have 1 plus 1 and this will be 2 1 plus 2 will be 3 2 plus 5 plus 3 will be 5 3 plus 5 will be 8 and 5 plus 8 will be 13 hopefully sometimes I write the European style of a 1 or sometimes the American style so let's maybe do the European style here so the ones would look like this that's okay okay let's go on so what do we see here we see we can calculate the Fibonacci numbers from left to right why can we see that? well that's how I just defined the Fibonacci numbers so B I said the first two numbers are given and then we just add up always two numbers to get the next one however we can also read this series of numbers from the right hand side going to the left so I can say the 13 here is the same as 8 in other words in order to get this one I need to know this one and this one okay and then in order to know in order to calculate the 8 what I need is I need the 5 and I need the 3 in order to get the 5 I need the 3 and I need 2 so always the two predecessors when I'm going from the right hand side and I'm asking the question what is this Fibonacci number and let's say I don't know it how do I answer this question well all I need to answer this question is those two numbers so I can look at this from a recursive way so to say so I can ask the question what is this number well this number is this number plus this one so I have to figure out the numbers from the right to the left which means we always add two numbers to get the next one there is also a pattern so to say from the right hand side we just have to know what are the two predecessors in order to get it so let's see how we can use this observation in code to code up the Fibonacci numbers so here we see also the Fibonacci numbers in code 4 144 will be the 13th Fibonacci number and this will have an index of 12 so note for what is to come these are the Fibonacci numbers and I will index them in the same way as we index in Python so the first Fibonacci number will have the index 0 the second Fibonacci number will have the index 1 and then 144 this is as I said the 13th Fibonacci number and this will have an index of 12 so just to make sure that you don't make any off by one errors here and let's write code a recursive code to calculate a Fibonacci number so here is the function Fibonacci it takes i now not n and i is supposed to be an integer and the integer is the index of the Fibonacci number to calculate so it's an index so it's a zero based index and then the function returns the IS Fibonacci number which is also an integer so let's look at what the code does here let's say I want to go ahead and maybe I also write the indices here maybe this is index 1 index 0, 1 this is index 2, 3 4, 5 and 6 and 7 this is how we write a 7 in Europe we have this dash here you don't do that in the USA for you I think this would be a 7 or close to a 7 at least so anyways let's say I want to calculate the 13 here which happens to be the Fibonacci number with the index 7 so this is the 8th Fibonacci number and it has an index 7 this is now what I want to calculate in this example I would be set so let's run the code in our head what would happen if I were to calculate this Fibonacci number well if I have 7 then the first case here is not reached then there is an elif and the elif seems to be the second Fibonacci number and 7 is also not equal to 1 so we go back to the last case here and this is obviously a recursion why is this a recursion well because the function calls itself here now the question is what is different to the previous examples we saw of the greatest common divisor and the factorial well the big difference is we have now 2 recursive calls so the Fibonacci number the Fibonacci function calls itself 2 more times another difference is however having 2 base cases in this example is also rather trivial why? well if I told you that the Fibonacci numbers are defined to be the first 2 Fibonacci numbers are this by definition and then only the third Fibonacci we start to calculate this means there have to be 2 base cases so factorial of 0 was 1 so there was only one base case for the factorial function but here we define the first numbers and that's why we have 2 base cases here so but again this is obviously a recursion and as I already told you this recursion will not be very efficient but it works correctly so let's see what this code does I define the Fibonacci function and now we call it I call it with the index 12 to calculate the 13th Fibonacci number so the 13th Fibonacci number will be 144 so let's execute this code and we get back a 144 so in other words even if we don't understand every bit of this code the function seems to work it seems to be correct however and this is a topic that will become important in the long run not for a beginner in the programming but I already want to expose you to it let's talk a little bit about how efficient an algorithm is or what is efficiency in the first place for an algorithm so what I have here is a double percent here and this is what in Jupyter Notebook is called a magic and this magic is named the time it magic and what the time it magic does is whenever you put this in the beginning of a code cell the code cell is timed that's what it does and then 100 here and what does the dash and 100 do well that means when I now execute this code cell Jupyter Notebook will execute this code cell exactly 100 times and time it 100 times and then it will give me the average run time of this code cell and a standard deviation so let's run this and I get back 82.1 and what is this so this means the function ran very fast micro is 10 times 10 to the negative 6 so it's a 0 and then we have 5 0s and then a 1 this would be a micro second and here we take 82 micro seconds so it's a very fast function obviously and here we can read that we had 100 trials here so now regarding what is the efficiency of an algorithm what do we mean by efficiency in the first place well there are two big topics when we talk about efficiency of an algorithm the first one is its run time so how fast is the algorithm and the second big thing is how much space in memory does the algorithm need so what we do here in this chapter here we only look at the run time so we time it that's why I use the time it magic and let's see and what happens if I try to calculate Fibonacci for the index 24 which means I calculate now the 25th Fibonacci number and I run the same code except that now 12 becomes 24 and we see already the star in the cell here that the code cell is still running so what is this and now the question is how long do we have to wait to see the result now we see the result so this cell was also executed 100 times and on average the run time was 18.6 milliseconds so a millisecond is 10 times 10 to the negative 3 seconds so it's 0.001 seconds that's what a millisecond is so in other words 1 millisecond is 1000 times longer than a microsecond in other words if I disregard the 82 and the 18 and we only look at the prefix of the second here on average calling Fibonacci 24 takes 1000 times as much time as calling Fibonacci of 12 and that's an observation so what we did going from 12 to 24 is we so to say doubled the input of the function and the run time crew by 1000 so that's a very bad trade-off here so doubling the input leading to 1000 times slower run time and that's an example of so-called exponential growth that is occurring here that is basically slowing down the algorithm and I think I have one more example here yes Fibonacci of 36 and as we can already see dash n only has 1 so I will only execute this code cell once and time it with that and we see that this also takes time it takes very long time and now you understand why I didn't call this cell 100 times so now this cell took 6 seconds okay that means going from 24 to 36 means we have another factor around about 1000 like the function called 1000 times slower again in other words going from the first cell of Fibonacci of 12 to the last cell of Fibonacci of 36 the function will be 1 million times slower and what happened to the input the input only tripled from 12 to 36 so this is again to just show a very bad run time we want to know why is this run time so bad and how do we do that we look at the Fibonacci function in Python tutor to understand what is going on in memory so let's go ahead we create the Fibonacci number now we call it for an index of 5 this is a problem that is significantly smaller than Fibonacci of 12 but it's already big enough we will see what's going on why something goes bad here so we call Fibonacci the first time around a new local scope appears I set to 5 and the function goes it skips the if, it skips the elef and then it hits the return statement here this is the last line and then in this last line the Fibonacci function will call itself two more times so what will happen is we will stop here on the left Fibonacci call so when the expression that is next to the return statement is evaluated this expression is evaluated from left to right of course so the Fibonacci i-1 is called first and whenever this has returned then only Fibonacci of i-2 will be evaluated so in other words we will now see this function call here this function call will be Fibonacci of 4 so let's click next a couple of times and now we see as I said the function is called again and we have a new local scope and i is now 4 so what does this function call do well it runs again via if via elef and then it hits the last line again and also here the left Fibonacci call will be evaluated first only once this is done the right hand side will be evaluated so what happens now is i is 4 so another Fibonacci call will appear where i is set to 3 okay so let's look at the memory diagram we have the first function call Fibonacci with i being 5 and this function is waiting for the Fibonacci call where i is 4 and this function call is waiting for the function call where i is 3 however the other function calls they are also not yet done even if this call returned why is that well the Fibonacci where i is 4 is not only waiting for this function call but it also will have to go ahead and calculate Fibonacci with i is set to 2 and the upper function call here where i is set to 5 once the Fibonacci where i is 4 returns to it this function call also has to make another function call where i is set to 3 and now we see already the problem so the Fibonacci call where i is 5 will make another call to Fibonacci with i is set to 3 eventually sometime in the future however we have a Fibonacci call with i is set to 3 already going on in other words there are many many problems that are solved again and again and again and we can also read this in the source code already so in other words for every time we call the Fibonacci function 2 more calls will be made so if i call Fibonacci once this leads to 2 more calls and those 2 more calls they will lead to 4 more calls and those 4 more calls they will lead to 8 more calls and those 8 more calls will lead to 16 more calls so in other words every time the number of function calls will double so this is exponential close going from 1 to 2, from 2 to 4 from 4 to 8, from 8 to 16 from 16 to 32 and so on so we have exponential close in the number of function calls so let's run this rather quickly now and then i will draw you another diagram where we will look at this from a different angle so now Fibonacci 3 is running this creates another call where i is 2 this creates another call where i is 1 and now i is set to 1 well this is a base case so this function will not make another function call this will now go away right away so now we see the return value so the base case is hit and this will return and now the Fibonacci call where i is 2 will make the second function call on the right-hand side now where i is set to 0 this is luckily also a base case so this will immediately return so it's called and it will immediately return and then this call here where i is set to 2 will also return because now this call has evaluated both the left-hand Fibonacci call Fibonacci i-1 but also the right Fibonacci call Fibonacci i-2 now this call all it has to do is add the 2 up pass it 1 up so now what we see is we have a sequence here function calls and it crew down and it now goes back up and then we will see that the sequence will go down again so in a way these function calls they will now go up and down and up and down and now down again and now back up and now the function is done so we see that we have way too many functions calls we are making the same function calls over and over again so this is what happens in memory and now as i said let's i will give you another diagram that will show you what happened so let's say i will call here so let's say i make a function call and set i to 5 and i abbreviate this just as F of 5 so what does this do well if i go back in the source code what this will do is this will skip the 2 base cases and go to the last line and then this will lead to another function call which is F of i-1 so this will lead to another function call F of 4 however that's not the only function call that has to be done this will also lead to another function call 5-2 so 3 in order to calculate F of 5 i have to calculate F of 4 F of 4 and also F of 3 and only if i get back the values for both of the function calls i can add together the results so this result will be added to this one and then i only know what is the result what is the overall result here now the problem is this in order to calculate F of 4 what we need to do we need to calculate F of 3 and F of 2 and we see the problem here in another angle so in order to calculate F of 3 let's say we calculate F of 3 here but we also calculate F of 3 here so in other words we have a tendency here and now what happens is in order to calculate this F of 3 i have to calculate F of 2 and F of 1 and luckily F of 1 is a base case F of 2 is not so this will lead to some more function calls namely F of 1 and F of 0 maybe put this in the picture and then this one is the base case it will lead to function calls F of 1 and F of 0 and then these are all the base cases and on the right hand side we will have the same this will create a function called F2 and F of 1 and the F of 2 will create two more function calls F of 1 and F of 0 so this is just another way to visualize to you with i of 5 means and now let's see how much redundant work do we do so on the right hand side right here, this is now a tree in computer science we would call this a tree and in order to, and let's analyze this so we have here this path here the F of 3 path and we also have this path here in other words this one here I already know that I can reuse it in other words let's use another color all of this work here is unnecessary the entire right half of the tree is unnecessary and then let's analyze do the same analysis on the left hand side of the tree so the F of 4 I don't see anywhere else the F of 3 do I see it anywhere else no and this F of 2 I see them again so I can actually get rid of this computation graph here or this part of the computation this is unnecessary and then here's the question is do I see any more F calls that have been made well here I have an F call to 1 with F of 1 call and here another F1 so I can also get rid of this F1 and so what I'm left with is just the numbers the Fibonacci numbers basically in sequence I have the 0 the first Fibonacci number 1, 2, 3, 4, 5 so in other words if I calculate the Fibonacci numbers from left to right as we actually defined them let's calculate the Fibonacci number from left to right then what this means in the tree is all I need to do is I need to calculate this first so this is my first calculation my second calculation my third calculation my fourth calculation my fifth calculation and then this would be my last calculation here in other words if I go this direction in other words graphically speaking I calculate the Fibonacci numbers from left to right then I won't run into this problem but we see so what's the learning of this Fibonacci numbers they are defined in a forward way from left to right but we can solve the problem in a backward way so again in order to get the 13 all I need to know is what are these two numbers so I can have a backward path in calculating this however as we see in this graph already every number has two predecessors and they unfortunately overlap that's why we do all the wasteful work here and then if I use the other representation in this graph here in this tree structure here then I think it is absolutely evident what is the big problem here we're doing repetitive work and in this case in the Fibonacci case the solution is we just calculate the Fibonacci numbers from left to right and we will see at the end of this lecture how we will do that in a recursive way and in a future chapter in chapter 8 to be precise we will see a way how we can calculate the Fibonacci numbers from right to left so in a backwards way without the extra work and how can we do that we can already see the point so the problem was is that we did not remember here in the tree that we did some calculation before so once I have done this calculation F3 here how about this how about I take the return value of this function and I write it somewhere in a lookup table and whenever I get to the right side of the tree I just look up the value and this is a concept called memorization and it's part of something that is called dynamic programming a discipline called dynamic programming and there are problems that can only be solved in a recursion there is no forward solution to it or at least no good one no obvious one and so with this additional tool that we will learn in chapter 8 we can actually solve problems in a backwards fashion so in this chapter in chapter 4 here the idea I present recursion to you as a way to iterate but I also present it to you and that's the second meaning of recursion in a backwards fashion because there will be problems in the future that we can only solve in a backwards fashion and then we have to develop more tools that we will implement in Python that will allow us to make the backward calculation efficient and again this is called dynamic programming and dynamic programming is often used in logistics and supply chain management to solve optimization problems and also there are some applications generally in economics and also most notably if you are into finance and if you are into option valuation then learning about dynamic programming and recursion is just a very basic idea within dynamic programming is also valuable because in order to evaluate a stock market option for example you also do that in a backward fashion where you take finance courses and then you will learn that and in order to implement what you learn in finance courses on option theory you can use dynamic programming which means at the end of the day you construct such a tree and you go it backwards so the take away is to look at the code here what do we learn here well recursion means we can also have more than one base case and recursion also means that it is a recursive call however this is often not efficient okay so in this context we also discussed efficiency of algorithms and I gave you just high level interview high level introduction into what it means to talk about the efficiency the run time analysis of an algorithm okay let's look now at some other case let's look at infinite recursion and infinite recursion is something bad so I will just try to make you aware of that so what's an example of an infinite recursion so a function like here run forever that all it does is it calls itself run forever and as you can tell by the name this function will call itself forever now the question is is this function useful and the answer is of course in this case no this is just a trivial example of showing to you what is an infinite recursion but infinite recursions will occur when you miss program a recursive solution to a problem so it could happen and it could also happen as we shall see soon in some other circumstances that you did not plan ahead so let's first run this function forever and now one of two things will happen either my computer will die and this recording is over because the function will run forever and so my computer just runs out of computational power out of cbu or the second thing that will happen is python may save me let's look at what happens the function keeps on running we see the star here and then eventually I get what python tells me is a recursion so python saves me here python has a built in detection for infinite recursions so if ever you run into an infinite recursion then python will basically break out of the infinite recursion and throw a so called recursion error and yeah that's what we see here luckily otherwise the presentation will be over by now so how can we get into an infinite recursion if I don't give you a toy example let's look at our countdown example again and remember this function was defined for integers so just as a short reminder before I execute this example if you are unsure what the function takes you can always use pythons built in help function and now you can read the doc string of our function right here and it says the arguments should be n and supposedly an integer but now let's assume that we as the user of this function did not read the doc string and we call this function let's say with a float and let's maybe not call it with 3.1 but let's call it with just 3.0 what happens what happens is the function spits out 3.0, 2.0, 1.0 so the function luckily works but let's go back to the example as it was set up let's call the function with 3.1 and what happens now now something weird happens it starts to count down from 3.1, 2.1, 1.1 and then it hits 0.1 0, 0, 0, 0 and a 9 and then it passes the 0 and then the fourth number will be negative 0.89999 and then it goes on with negative 0.9 so two things that we observe first by giving the function a number whose value is not a whole number not an integer it will not hit the 0 so the countdown won't fire off that we don't hit the Happy New Year and also observe that the numbers the floating point numbers here 0.100 and minus negative 0.8999 they are not precise and this is also a precursor for what is to come in chapter 5 where we look in detail into numbers and how they work in memory and you will learn most notably that you cannot expect floating point numbers to be precise and this is an example here but in this chapter the more important observation that we make is that we messed up the function in the way we called it we called the function with 3.1 and it didn't work because the function didn't hit its base case and because the countdown function did not hit its base case it ran forever and also observe as I showed you before if I ran the function with 3.0 luckily it works so let me use this as a chance to tell you about an idea called duck typing so what is duck typing so in many programming languages when you specify a function when you define a function you have to explicitly specify what the type of the argument will be so in the definition you will say this parameter is an integer, this parameter is a float and then at runtime the programming language will enforce this so in many other programming languages when you say the countdown function takes an integer then we are not allowed to pass in a float we would see an arrow here however in Python Python is a little bit more easy going here Python accepts any object as the argument even though we expect here in the function clearly an integer we can technically pass in any object we want and then as long as the object that we pass in behaves like an integer the function can work with it and the float 3.0 obviously behaves like an integer and whenever some object of a different type behaves like an object of a certain type that we are looking at that we want then we call this duck typing when saying among programmers goes if it walks like a duck it must be a duck so this saying basically emphasizes that in Python the behavior of an object is a lot more important than its type and we will get into this in a lot more detail this is one of the reasons why I like to talk about abstract concepts in this course such as we saw in chapter 2 I introduced the abstract concept of a callable and I told you that there are three examples of a callable namely built-in functions built-in constructors but also user defined functions and the abstract concept is any object that is callable is called a callable and in the same way as I classify those three examples into one concept we will classify many many more concepts and we will already start later in this chapter actually but the idea is that the types themselves they are not that important it's more like the one point thing is how objects behave and in this example the integer 3 and the float 3.0 they behave in the same way which is why the function works however if we are not careful a float with a decimal here then obviously we get an error message here and then the question is what happens when I scroll down here how far does it go well the output is cut out here we see the code here wanted to print out 10,500 characters but only 500 are shown I have a plug-in here that cuts the output just for obvious reasons but then we still see the recursion error so it's the same error that we saw before in the countdown example or in the run forever example excuse me so let's look at some other function let's look at the factorial function and let's call this function with 3.1 what happens well the factorial function will also run into a recursion error and why do the factorial function and the countdown function run into this recursion error well in the base case we checked if n equals 0 and by doing that we basically require only hit the base case if the n is exactly 0 and in this case if we decrement a number that is not equal to a whole number in this case 3.1 in increments of 1 we will never hit 0 and because we don't hit 0 this doesn't work how can we improve the functions well we could improve the function in a way where we say well as long as in a way that we modify the base case such that we check if n is smaller than or equal to 0 so in this scenario once the function once n is below 0 it will stop but nevertheless the output would be wrong so passing in the floating point number is simply wrong for countdown because it doesn't make sense to print out 3.0 and so on so how can we work on the function how can we improve the functions so that they don't run into a recursion error well we do that by checking the type of the argument and validating the input that a user of our function passes in so here is a new version of the factorial function it still takes an n and now we have some more code up here two more branches in the if statement and the first branch says if not is instance n int so what this means is if instance is a built-in function it takes two arguments the first argument can be any object like n and the second argument is what we refer to in chapter 2 as constructors and to remember that the type of a constructor is type in other words int is a type it's a data type so in other words if instance takes two arguments the first one is any object and the second one is any type and then the function returns true and the object that we passed in is of this type and it returns false if this is not the case so in this case in other words if the user passes in an integer or an n that is not an integer then we run into this branch and what then happens is we read race type error and the race is another statement which basically raises an exception which basically means Python shows us an error message and then in this case I use the type error because whenever a user passes in something into the factorial function that is not of type integer then it's just the wrong type so it makes sense to call this a type error and then I have a custom error message which says the factorial is only defined for integers okay and then I have this elif branch here and here I check what do I know here for the highlighted code here I know that in this area n is definitely an integer so if it's not an integer we stop here the race statement of course aborts the function it's kind of like the return the return breaks out of the function and returns from it and the race statement throws an exception which stops running in other words once we are down here we know for sure that n must be an integer and then because n is an integer I can use relational operators and in this case check if n is positive or not and if n is smaller than 0 I raise a value error value error because the value of the integer is wrong it's negative and in order to calculate a factorial I must have a positive number so I provide a custom error message and say the factorial is not defined for negative numbers so these two branches they implement first of all type checking here and the elif branch implements input validation so first we check if the type is correct and then we check if the object that is of a correct type also is of a correct value for the function to work the third elif the third condition here this elif n double equals 0 this is just the old base case so before in the old implementation we had this it said if and so on this code would also work but whenever we have an if elif here and we have an if here it may be applicable to just connect it to into one big statement but the other implementation would also work so in other words if we are down here we just run the function as before but now we know for sure that n is both an integer and it's positive or at least non-negative and if we know that then we know for sure that we can calculate the factorial so now what I do is I define this function and it's tested so let's check the factorial of 0 because 0 is of course an edge case when testing the function so now we get back a 1 if I calculate the factorial of 3 it's still 6 and now if I calculate the factorial of 3.1 I get back a type error and the function complains and says factorial is only defined for integers and if I call the factorial with a negative integer I get a value error that's fine for non-negative numbers let me go back to the type error and reiterate some point here that regards the duck typing that I mentioned so if I use let's say 3.0 and I execute the cell again you will see that of course we also get a type error and that makes sense but it doesn't make sense because I just allow int types what this function unfortunately does it does not give me as the user of the function the flexibility to pass in a float object that basically behaves like an integer so duck typing is now obviously disabled in other words passing in 3.0 you know should work because the value of 3.0 at least for us humans would be 3 of the number 3 should be defined so now the flexibility that many Python functions allow is not there anymore I have to pass in an int and so even though the 3.0 here walks and quarks like 3 it doesn't work and if you want to become really good in Python programming then you should always have in the back of your mind duck typing and you should always assume that whoever uses your code and that could be yourself of course should be allowed to flexibly choose what type of object they pass in and as long as the object behaves in the correct way we want to allow that and here we clearly don't allow that so for now we leave it with that in chapter 5 towards the end we will talk about abstract base classes and there is a concept called goose typing goose typing is related to duck typing and goose typing will help us allow to re-sector this function into a way so that we can also call it so in chapter 5 we will see how we can change the factorial function such that I can call it with an integer 3 and a float 3.0 and the same result 6 in both cases and whenever I call the function even in chapter 5 with let's say a factorial of 3.1 I also will get a type arrow in chapter 5 because the factorial does not make sense if the decimal is not all 0s that was the first side of the coin that was a recursion so I think the examples here were you now know what is recursion and I just reiterate this recursion means usually that we view a problem from its end and we try to solve the problem in a backwards way so to say and now we do the opposite we will solve a problem in a forward way and in this course I will call this approach looping and both recursion and looping are two sides of the same coin and the coin is iteration and iteration just means we run some code repetitively ok so we will start with the while statement you know from chapter 1 already from our very first example in this course that we can loop with the for statement but the for statement I haven't officially introduced yet in this course so I've only shown to you an example of how it works I've introduced it here and we haven't really talked about how the for statement works and really we don't need the for statement we can get away with the while statement all alone and we will see how the for statement is just a special case of the while statement so let's start with the while statement what is it to show it to you let's look at the countdown example again and now instead of a recursion we use looping to solve the problem here is code here is the countdown function again it still takes an integer n and now I also included type checking here and input validation just so that you see more examples of how to do that and then the actual looping occurs down here so the code up here we remove this is a code that only is like making sure that the function is called with good arguments but the actual business logic is happening in the code that you see as of now but of course I will just leave it in in the actual version so let's analyze the while statement what is the while statement well the while statement is a compound statement just like the if statement meaning it consists of a header line a header line means we have a keyword while in this case just as in the if statement we have a condition and then we end the line the header line with a colon and then we also have a body that is executed repetitively and the body is also indented by four spaces and that's it and then once we stop indenting code it's not indented anymore this is already outside the while loop and I call this the while loop sometimes because it's basically what this does so let's assume we call countdown with n set to 3 and we hit the while statement so what happens well what happens is this condition here n not equal to 0 is evaluated and if n is 3 then the condition is true which returns true and then whenever the rule is this whenever the condition is true what happens is we execute the code in the code block so that means because n is not equal to 0 we go to the print here we print out 3 and then what we do at the end of the loop we take n which is 3 and decrement it by 1 so n will be 2 after that and then what happens at the end of a while loop is the flow of execution just goes back up to the while statement to the header line and then with our new n which is now 2 we evaluate the condition again and now 2 is not equal to 0 is also true so we will run the code block here one more time print out 2 decrement the 2 n is now 1 and then we go back up to the header line compare 1 to 0 it is still not equal to 0 we run it one more time print out 1 and now decrement 1 by 1 to reach n of 0 and then we go back to the condition so we always once we hit the last line of a while loop we always go back up to the header line and now we evaluate the condition again and now n is 0 and then I compare 0 to 0 and it is of course the same that the condition is false here and whenever the condition is false what happens the while loop stops and we continue with the flow of execution below the while so the next line of code that will be executed is just print happy new year so again the n comes in it passes the input validation and the input checking it hits the while loop and it loops that's where the loop comes from it loops around here and it goes back to the previous times until the condition here is false and once this condition is false we continue with the flow of execution below so let's define a function and hit and call the count on function again with 3 and it works now just to show you something we go to Python 2.0 one last time in this chapter and let's look at the exact same example here just without the doc string and let's see what happens in memory the first step creates the count on name referencing the function and then the function will be started and now we get a new local scope here and any set to 3 and then we will pass the input validation and the type checking here and then we will hit the while loop and then let's see what happens so we pass the validation and now we hit the while loop and now the condition is evaluated and it's of course true because of that we hit the next line we just print n and we execute this line and we see the 3 output and then we will decrement the n by 1 and then n is 2 and then the condition will be evaluated again it's still true and so on so now the code is executed again here comes 1 and then one more time the condition is evaluated and then we print out the 1 and then n gets decremented one more time to be 0 and then important now we go back up again and we evaluate the condition again but now the condition is false and because of that we jump to the print happy new year and then the function is done which we see with the return value here and because there is no return statement here the return value is again set to 0 and the function returns and the scope goes away however note one big difference to a recursion when I let's go to the very beginning and go over it rather quickly we only have one function call going on in memory at the same time and the n is actually updated so it's 3 and it becomes 2, it becomes 1 becomes 0 and then the function is done in the recursive version what we had is we had different function calls so the countdown with n equals 3 a function call will call countdown with n set to 2 so I have several function calls I had several function calls here in memory several scopes with always the same name so what we what we take from that is that an iterative or a looping a looping implementation of the countdown function and this holds for other algorithms as well is often a lot more memory efficient because we only need one scope and one name and we don't that's just a lot more memory efficient and this is something that holds general for recursion versus looping and this is why if you go to platforms like stack overflow and ask the question what is better recursion or looping many people will say well looping is better because it's more memory efficient that is why however just a note if you do this kind of research you will definitely run into people that will tell you please use a so-called functional language and that follows the functional paradigm and not the object oriented paradigm and in these other languages doing recursion does not incur this cost in memory so there are other programming languages that can actually work with recursion in an efficient way, Python is not one of them so in other words whenever you want to write an algorithm to solve a problem at production scale at a big scale then most likely a implementation with a while loop or a for loop later on will be better than with a recursion but for a beginners problem it is still important to learn about recursion because there will be problems in supply chain management for example that can easily be solved or rather easily be solved with dynamic programming going backwards so with a recursion and that are not so easy to solve in a forward way and because of that you definitely should know about recursion because again many supply chain problems can be solved with it it's an important concept even though in Python it is not as memory efficient as a while loop okay let's continue so let's look at the other examples that we saw before as a recursion now we look at them with a loop and whenever whenever we have a recursion we call an algorithm a recursive formulation of a problem and whenever I use a loop a while loop or later on a for loop to implement an algorithm you will hear me say that is an iterative version of an algorithm so now we look at an iterative version of Euclid's algorithm and what do we see here well after the type checking and the input validation we have here a while loop and remember that Euclid's algorithm calculates the greatest common divisor of two numbers a and b and then the algorithm goes like this as long as a is not equal to b we take we subtract the smaller the two numbers from the bigger one so if a is larger than b we subtract b from a otherwise we subtract a from b and then the solution to that after this while loop is over so after both a and b are the same number then we just return a but we could as well just as well return b down here because once the while loop is over we know that a and b are the same value so that's the iterative implementation of Euclid's algorithm and now back in the recursive version I told you that I have a hard time to see how this algorithm intuitively works because back then it had a modulo operator in it and it was just not as intuitive to follow I personally also have troubles to see how this algorithm works even though if you check the Wikipedia page for this algorithm there is a nice visualization and I think you can follow this so this algorithm if you study a little bit you can intuitively understand but you couldn't understand it with some other help here so why did I put this example in the presentation well usually after like the first couple of times I gave this course some students asked so is it true that the iterative version is always easier to formulate or is it also often the case that the iterative version is better than the recursive version because of the limitations that I just mentioned the memory limitations and I know that this is not the case so in some situations the recursive formulation is actually really better than the iterative implementation and Euclid's algorithm is one such example so let's look at the test cases we saw before let's calculate the greatest common divisor of 12.4 this is still going to be 4 and then let's go ahead and test the edge case of 2 prime numbers so this should be 1 and it is 1 and now we continue the study of efficiency of algorithms now we calculate the greatest common divisors of those two large numbers and if you go back in the presentation you will see that I calculated the greatest common divisor of exactly these two numbers before and it was super fast remember so this algorithm did not take too long and now if I run the iterative version of the algorithm we will see that now this takes some time and it takes very much time actually so it's over 7 seconds to find the greatest common divisor so this is an example where the recursive formulation of the algorithm is actually faster than the iterative version so this will answer the questions that you may have after this lecture is the iterative version always better and the answer is of course it depends and of course it's not true in general so just as we had infinite recursions we can also run into an infinite loop and usually you run into an infinite loop if whatever condition that you put into the header line of the while statement is somehow wrong and we usually call this an off by one error and this is usually so this is usually the programmer's fault you have like a bad implementation and because of that your code will run in an infinite loop however there are also cases where we don't know if an algorithm will ever return or finish for example there is a a game which in math goes by the name collats conjecture so that means we will see now a game let me just introduce the game so that you know what I'm talking about so let's play the following game you will think of a positive integer n can be any number and then I give you the rule if n is even then you go ahead and you just divide n to get the next number if n is odd you multiply the old number by three and just add one to get the new number and you repeat these these two steps so long as you reach the number one and then the question is do you always reach the number one and mathematicians try to prove this and still to this day there is no proof for this so just this simple game cannot be shown if it always gets to one or not let's implement this game in Python so it's a little bit longer but the reason for that is because I wrote a nice doc string here so that if you copy paste the solution this function is basically ready to go for the game we have input, we have type checking we have input validation so n must be strictly positive and then here is the formulation of the game that's the important part so what is the condition in the while loop well the condition is n not equal to one because that's what we want to show so remember in the game the idea is we start with any positive number n and we modify this number n by two rules and the question is do we ever reach one or do we always reach one that's the question so we assume here because of that we have to keep on playing the game we have to apply the two rules so long as we have not reached one so in other words as long as n is not equal to one we have to execute these two steps here and then I do the following I first print out the n so that I see some intermediate output and then I check if the number n that I currently have is even that's what I do here I use the modality division by two and if there is no rest it's even and then what I do is I divide the number by two and replace it with this number so in other words I update n with the double slash equal operator or statement so why the double slash well we know by now that if I divide a number by another number with the single slash then I will always get back a float but here I don't want to get back a float because we've seen that floats are not precise and we will study that further in chapter 5 so I use the double slash so that the division will always return another integer and I know for sure that once I'm here in this line of code that I can be sure that there will be no rest why because I've actually checked that a division by two works so this will just divide by two without rest here because it's an even number in case where we have an odd number what I do is I take the old n I multiply it by three and I add one and set it to my new n and I play this game so long as the n is not yet one and once the n is one this condition will be false and the loop will stop and then at the end I will just print out one because the last n will then be one of course so I define the function to play the game let's start for example with the number n is equal to 100 so these are the numbers 100 is even so we divide by two to get 50 50 is even we divide by two to get 25 25 is odd so we multiply by three as one to get 76 and so on and we see that at the end of the day we reach the number one and the question is the theoretical question is do we always reach one with the number n for example 1000 the series gets longer this kind of makes sense to us because if we start with a higher number reaching one should take longer but we reach one what happens if I call the function with 10,000 I also reach one but the series becomes shorter again and you see that is a problem that's one of the reasons why it's so hard to prove mathematically if this game always reaches the one so far nobody has proven it so there is actually prize money on this game so if you can prove mathematically that this game always reaches one you will make some money however I have played with this game quite some time and I've always reached one so far however that does not constitute a mathematical proof because just because I show that the game works for a number n doesn't mean I can tell anything about the game working for n plus one ok then next so we've seen the while statement at work and now let's see look at the fourth statement the fourth statement is really what is called syntactic sugar for the while statement so what does syntactic sugar mean syntactic sugar in a programming language is syntax that we don't need to get to do any new features or to implement any anything new in other words syntactic sugar are any syntax or construct in a programming language that don't provide any new capabilities to the programmer however they make life easier for the programmer by simplifying some use cases so for example most usages of the while statement I kind of like work this I am giving a sequence of numbers for example and I do something for every number in the sequence so I could loop over a sequence manually with the while statement but the fourth statement as we will see is really just an abbreviation so let's look at the example so I have here a list elements with the numbers 0, 1, 2, 3 and 4 and now I want to loop over these numbers and print them out one by one using the fourth statement so we have seen plenty of times how we would do that with a fourth statement but now we want to do it with a while statement just to illustrate that you don't need the fourth statement so how would we do that well maybe let me insert a new line a new code cell here so just to repeat some concepts now elements is defined it's a list I use the index operator which are the brackets and let's say I want to get out the first element in the list we start counting at 0 so the first element in the list I will get back as elements index 0 so this gives me back a 0 and just to illustrate the point that this is not just a 0 that we get back I just overwrite the first element to be 99 and if I now ask for the first element this would of course be the 99 as well you get the point if you don't go back to chapter 1 and look at the example who am I and how many in this chapter so now this is 0 again so now I want to automate this and I want to go from beginning to end so how do I do that well I start with the index 0 and I have to go to the end so how do I figure out what is the end well I figure out the end length function to build in length function gives me how many the number of elements in the list so in this case 5 elements and now the question is what is the largest possible index when I want to index into the list can I index with the number 5 and the answer is no because when I start counting at 0 I can only index 2 length of elements minus 1 this works this is the last element in other words this would of course be 4 ok that's indexing a short review on indexing and now let's use a while loop to index so at first I set up a temporary variable called index that is a variable I don't really want but I need it so I set it to 0 because I start indexing in the first element and then I set up a while a while loop and the condition is as follows while the index is strictly smaller than the number of elements in the list do the following and then what do I do well I use the index operator here in order to retrieve the next element by indexing so I use the indexing operator and I get the element so back here I have the elements list and here I have a single element singular and then what I do with the element I print it out and I end the print function with an empty space so that means I am printing all of the numbers on the same line and then in the last line I have to adjust my index so in this case I increment the index by 1 in the count down example before I have to decrement the variable in the while loop by 1 here I have to increment the variable so I say index plus equals 1 this is what considers the while loop and so how does it work so it starts at 0 we get the first element we add 1 so index is 1 the condition is still fulfilled we go through it again and at some point index will be 5 or actually it will be 4 what happens is 4 is strictly smaller than the length of elements which is 5 so index of 4 will be an index that works and then we go back up to the condition and index will now be 5 because we increment it by 1 and then 5 is not strictly smaller than the length of elements which is why this code block will not be executed one more time when index is 5 so in other words this loop will be executed for indices between 0 and 4 both including and then at the end after the while loop because I don't really want the index variable what I do here is I just dereference it so I don't want the index variable to exist after the while loop so let's execute this code and I see the output and of course I can modify the output for example I can put a plus and then I have all the numbers on one line and pluses in between ok so let's go back without the plus maybe it looks a bit better so now how can we do the same thing with a 4 statement well you guessed it probably I just write 4 elements singular in elements plural and then print element I actually just code so I get the same output so you get the point and then this is the body of the for loop here and this is exactly the same as this line of code and now the observation to make is this line here in the for loop this one line the header line in the for loop replaces those three lines and those two lines so in other words the for loop just saves us a total of four lines every time we use a for loop and this is just to say that the for loop is just a special case of the while loop ok and we will get back to the for loop in detail in much more detail in chapter 7 when we talk about so called iterators so we have not yet learned everything about the for loop there is actually a lot that still comes with the for loop ok so let's continue and what I have here just to also make this explicit I create a list so let's maybe use our memory diagrams and quickly draw a picture what happens in memory because I want to show you some effects in memory as well so what the first line does when I execute elements is equal to this list what happens is a list object is created and we know that a list object looks like this it's this big so called array of something and an array consists of equally sized slots and of course the type of this thing is a list and then I have somewhere int object with the value 0 and this is referenced from the first slot then I have somewhere an int with the value 1 and this is referenced from the second slot and so on let me quickly draw everything and also observe that it is absolutely intentional that all the int objects are not somewhere aligned so they are a little bit random in memory we don't know when memory they are and then once the list object is created the name elements is put on the list here and it also gets a reference to the list object here so this is what we created in memory and we looped over the individual 5 objects here now let's contrast this with something else the so called range object or the range built in so in order to loop over the numbers 0, 1, 2, 3 and 4 I can also say 4 element in range 5 print element so let's execute this and I get the exact same output as before and so what is range really so range is a built in that returns an object and the object we will see is of type range into this in detail in chapter 7 and what is range so what is a range object let's create one so as I said the range 5 is an object of type range and if I only write here range 5 and execute this cell I get back a range object so what is the range object a range object is this a range object let's say is an object like this it is of type range as we saw and in this case I don't have a name here so maybe let's make up a name let's say r equals and then I have a name r and the name references the range object here so just to reiterate here or to emphasize the difference here a list object contains references to all the object and the important observation is all the objects in the list they exist simultaneously with the list together so the elements list which contains 5 objects means that 6 objects exist in memory simultaneously the list object was 5 int objects for the range for the range object only one object exists and what happens if I go ahead and loop over and loop over the range here what happens in every iteration of the for loop Python as the range object please give me the next your next integer so what that means is as this for loop here executes I get back in the first iteration just one so let's just do it again I just maybe loop over just r because I just created r and we see we get the same output so what just happened is the first time around the int object let's move this up a little bit the range object creates an int with the number one in it and makes element singular reference it and then I reach the second iteration of the loop and what the range object does somehow in memory it removes this one here and you move the reference and it creates another object with a 2 in it of type int and makes the element name reference this one and if I go into next iteration the game goes on like this so a big difference between a list and the range is that in a list the object exists simultaneously and I call this in this course the list object is materialized meaning all the objects are there in ones and zeros in the range object the objects are not there but they get created on the fly as I loop over the range I get the individual objects back so in other words the range object is highly memory efficient whereas the list object is not lots of memory available for all the objects in it and so the question is why do I tell you this well the thing that you should observe is that just as I looped here over a list I can also loop over something that is a range so let's maybe go back and type range here so I can what I'm saying is I can loop over different types of objects that's the important observation and remember duck typing which I introduced to you some moments ago duck typing means when I have two objects of different type that behave in a similar way in one aspect and there is one aspect in which in this case the list and the range behave alike and the aspect is within the context of a for loop so in the context of looping the object and the range object they behave alike although they are different types of objects and let's continue another example so just to illustrate what range can do let's say I want to loop over the list one three five seven and nine so in other words I want to start at one and I want to loop over every other number so this of course works however this code what it does before the loop starts it will go ahead in memory and it will create a list with the numbers one three one three five seven and nine in it so it will just basically model it as if it was this list here now how could I loop over these numbers without creating them all simultaneously well I can use the range built in and I can tell the range built in well please loop or please go from one to ten in steps of two and as we have not yet seen maybe but the left hand side the one is including of course and the right index the upper index is not included and the names for these three arguments is the start value the stop value and the step value here so if I execute this code here I get the exact same output but again with the implication that in the first code cell all the objects exist in memory simultaneously and the second code cell only two objects exist at most in memory so first only this object exists the range object and then the range object spits out the integers one by one and when an iteration the loop is over the end object goes away and the next end object comes into play so the second for the range is a lot more memory efficient that is an important observation and now as I said that this has to do with how different types of objects behave let's give the behavior a name so anything I can loop over in Python is called an iterable that's the technical term and you will find this term in the Python documentation a lot so whenever a function requires an object over which it can loop the documentation will say please pass in an iterable object and I contrast this with another behavior which I call the container behavior and this is just to illustrate a point further so let's look at another example let's look at the example of a new list called first names which holds five German first names and let's generate this and now I can ask the question is this name the first name Achim in the list first names and the answer will be true so in other words what is in here well in is the so called membership testing operator so in checks if the object on the left hand side is contained in the object on the right hand side so what I'm looking at here is I'm not focusing on the fact that I can loop over first names I am focusing on the idea that first names is a container and I can ask the question is something contained in the list in other words the list type that we have seen a couple of times but no it's not only interval it's also a container it's both so the important idea here is I just want to slowly make you slowly prepare you for what is to come sometimes I focus on behavior and for the behaviors we will use technical terms and here the list type has two different behaviors that we like and the one behavior is that it is an interval we can loop over it and the other behavior that we like is that it is a container and a container means I can ask it if it contains something and for example if I ask for my name here Alexander then it's obviously not contained in the list so we just know now that interval and container are two abstract concepts and we will see that very importantly a list object has in total four such properties or four such abstract concepts and they together will make the meta concept of a so-called sequence and this is what chapter 7 will be about so we will focus a lot more on these abstract behaviors so again this is nothing new I can loop over first names because first names isn't iterable so whenever something is iterable maybe let me show you something in the Python docs library reference the built-in functions here there is a built-in function or built-in called enumerate and just to illustrate the point why am I teaching you these technical terms actually well as I said in the introduction part to this course I want to prepare you to read the documentation on your own without the help of a more experienced programmer and the term iterable occurs quite often in the documentation for example with the enumerate built-in and the enumerate built-in is a very nice thing to know whenever we need indices and don't want to manage them on our own so enumerate as we just saw takes an iterable and we also just learned that a list object is an iterable which means I can pass first names the list to enumerate as the first argument because it's iterable and then what enumerate does it allows me to loop over something but not only over the elements of the list in this case but also over the indices that are provided so enumerate spits out or generates indices for something that I am looping over and I tell enumerate to start the indices at one so let me execute this code cell so now I'm still looping over all the five names but now I have an index for every name and the index is automatically provided by the enumerate built-in and changed the start let's say to 10 then the indices start at 10 so in other words when I just showed you how the fourth statement is a special case of the while statement we had to manage the index variable ourselves within the while statement but whenever you feel that in the program you write you want to work with an index you shouldn't manage this on your own you should always use the built-in enumerate to basically have indexes available for something you can loop over you don't have to do this manually you don't have to increment indices with plus equals one all the time this is done for you automatically okay so let's now look at a iterative version of the Ciponacci numbers so again the Ciponacci numbers are the sequence 1, 2, 3, 5, 8 and 13 and so on of course these are the first 7 or the first 8 Ciponacci numbers so how can we calculate the Ciponacci numbers going from left to right because we saw when we calculate the Ciponacci numbers from right to left this will result possibly in an exponential growth of function which we don't like so we rather want to calculate the Ciponacci numbers from left to right so let's do that in code with a fourth statement a for loop because we don't need to check for some condition as we did with the while loop the for loop is a lot more convenient here so how does this function work well first here I have to the input, the type checking and the input validation but just to make the function a bit shorter so this is just you know just making sure that down here the I that is passed in as the argument is a positive integer and then what happens in the code in the code block also function well I start with two variables A and B and I set them to 0 and 1 and what are 0 and 1 well these are the first two Ciponacci numbers that are by definition 0 or 1 and then I just print them out here print AB just so that I have some intermediate output so also I already tell you this beforehand when you calculate something in a forward way in an iterative way you can often put in print statements or print a function cause so that you have seen some intermediate results and this is often not so easy to do with recursions so here in this example here you have calls to the print function and as you see with the comment I added them only for deductible purposes in a real solution you would just leave out the print here because you don't want to have a side effect of prints you only are interested in the return value at the end and then so we start with A set to 0 and B set to 1 and then what do we have to do let's say I want to calculate the Ciponacci number with the index 7 so the 8 Ciponacci number the question is now how often do I have to loop well let's check here in the example here so if I'm given 0 and 1 I need one more step here 2 3 4 5 and 6 so let me rephrase that in order to calculate the Ciponacci number with the index 7 I need 6 loops right the first numbers are given so 1 2 3 4 5 6 6 loops so how do I get to 6 in this case well let's say I is 7 as I said so I want to loop over over the range in this case because I don't even need all the numbers one by one I just want to loop over them so what I do here is I say I minus 1 so again if I is 7 and I want 6 loops so how do I get to the number of loops but it would be 7 minus 1 which is 6 so here in the range built in I just type minus 1 so this would be 6 in the example and then I go into a for loop and then there is something that we have not seen before the target variable in the for loop is an underscore here what does the underscore mean well this is a convention so underscore is a legal variable name so this actually works but it's a convention to communicate that we are not interested in this number here in this variable in other words the range built in will give us back the numbers 0, 1, 2, 3 and so on and we don't need them and if we don't need them we just show that with the underscore here okay so and then what do we do well then we go ahead and we create a temporary variable and we get it by adding a plus b so in other words what is temp well temp is the next Fibonacci number in line we take the two old Fibonacci numbers to start with 0 and 1 and we calculate the next Fibonacci number so in this case when a is 0 and b is 1 then the next Fibonacci number temp would be the sum of a and b so this one would be 0 plus 1 this is how we this is why we calculate temp this way and then what we do is we set a to whatever b was set to and then we set b to whatever temp is setting to so in other words what I do here is I start with a and so let me where can we put it maybe let's say here so here's a and b so we start with a and b with 0 and 1 and so then we go into the first iteration of the loop I calculate temp and temp is 0 plus 1 which would be 1 and then I go ahead and I take b and I write it here to where the a was pointing to so a is now 1 and then b is set to temp so this one goes here so this one goes here and this one is gone okay so in the next iteration of the loop so the loop runs I'm here and then I have some print which is not important for this problem I go back up to the 4 to the header line and run it all over again and then I calculate my second temp and the second temp would be 1 plus 1 so this would be so temp is now 2 and then I go ahead in the next two lines and I set a to b so b is now 1 so a will remain 1 and then b is set to temp so b will be set to 2 and then one more time I will do it in the next iteration of the loop I will calculate temp as 1 plus 2 which is 3 so we overwrite temp and then I go ahead and I use the b which is 2 and I put it where the a is 2 and then I set b equal to 3 so in other words I only keep track what the for loop effectively does is I only keep track of the last two numbers in the line and that's all I need to calculate the next Fibonacci number 9 so at most I will always have three numbers so the last two that I added and the next one which is temp and then I just reset all of them and I calculate the next one in other words I just calculate always a Fibonacci number this is what the for loop does and again if I want to calculate the Fibonacci number for example with an index of 7 I need six iterations because the first two numbers are already there so I don't have to loop over I don't have to make any loop here in order to calculate those numbers they are already given okay so let's go on and calculate Fibonacci with an index of 12 the 13th Fibonacci number and it's still 144 as before but now I also see this nice intermediate result and this comes from the fact that I have the print functions in here for debugging purposes as I call it for deductical purposes so that you see how the Fibonacci number grows and then the 12th or the 13th Fibonacci number is just 144 and now let's look at the efficiency of this new algorithm again so this iterative version of the Fibonacci algorithm is now extremely efficient so let's calculate the 100th Fibonacci number and we get it the only problem here is I don't see the cell output why because I configured my Jupyter notebook or my Jupyter lab here such that the output is limited so maybe let's go ahead and calculate Fibonacci of 36 this is the one that took around about 7 seconds to calculate in the recursive version and now I get this number and it works and if I go let's say 250 to give you one more this also works and the algorithm is super fast and we have you know and the reason why it's super fast is because we simply calculate the Fibonacci numbers in a linear way and we don't have this exponential growth here let's go on and let's also look at the factorial function in an iterative version this is actually even simpler so here the revised version of factorial still takes a parameter n it does type checking and input validation I will remove it for now so that you can basically read it and then what I do here is how can I calculate the factorial in a forward way well maybe let's take a new piece of paper and see what is the pattern for the factorial so let's say 3 factorial is defined as 3 times 2 times 1 and then also technically times 0, no not times 0 it's times 0 factorial of course but of course this would be 6 so the thing is this in the recursive version what I did is I calculated the factorial in the direction from descending order so I started with the large number 3 and I used the descending order but in multiplication I can of course also say that 3 factorial is the same as 1 times 2 times 3 which is of course also 6 so I can go the other direction so to speak and so what does this tell me well this tells me that if I start with a product that I set to 1 and the reason why I set this to 1 is because 0 factorial is 1 and then I just go into a for loop and then I run the for loop exactly n times y n times well I start the for loop at 1 so this would be this one here and let's say I want to calculate the factorial of 3 so n is 3 I want to have 3 iterations in the loop and the iteration should have 1, 2 and 3 so in order for the for loop to get to 3 I have to have an upper limit of 4 here because we learned that the range built in has its left argument included but its right argument is excluding so this is the start number 1 that is included in the range and this is the stop number of 4 which is excluded so this will create a for loop going over 1, 2 and 3 and then we go ahead and we basically update our product so in the first loop so in other words our product starts out as 1 so our product starts out as 1 and then I go into the first loop here and that means I multiply this by 1 to obtain 1 and then I go into my second loop which means I multiply this by 2 so I have my product of 1 times 2 now which is 2 and then I do another loop here and then I have the 2 here and I multiply this by 3 to get 6 and this is how I get the factorial in a forward way and note that these are the intermediate products here so I call this here kind of like this is similar to a running product so to say so we've seen in the first example in chapter 1 what a running total is and what a running total looks like and this is kind of like a running product so I always multiply on the next number starting with 1 so 1 times 1 is 1 then I take the 1 as an intermediate result I multiply by 2 this is my next intermediate result and then I multiply it by 3 here to get 6 which is the next intermediate result so these are the running products so to say and let's see if this function works and I also just to note I put in one print here so that I also can see the intermediate results, the intermediate product here so let's try out this function I call factorial with 3 and I get back a return value of 6 this is the output here and then up here I see the intermediate product and these are the exact same products 1, 2 and 6 and this is what I see here as well ok so let's go on so now we have seen the for loop the for loop is a special case of the while loop and now let's see how can we customize the for and while loops that I'm about to show you I will only show you for the for loop but everything that I'm about to show you also works for the while loop so let's look at the following example let's say I have my list numbers again that we know from the first chapter the numbers from 1 to 12 and I ask the question whether or not whether or not any of the numbers has a square that is greater than 100 I'm asking so how can I write code that checks that so let's start I create numbers list with the numbers that we know from before and now this is the code that does exactly that so what does this code do well first it starts by setting a variable is above to false so our initial assumption is that none of the numbers here in the list is a square that is greater than 100 that's our initial assumption and then I loop over the individual numbers one by one and I print them out for deductible purposes again and then for every individual number I I take I raise it to the power of 2 so number to the power of 2 and I check if that is strictly greater than 100 because that's the question and when that is the case I set is above to true and then after the for loop I have a compound ifster statement which only checks if is above is true or false and if it is true then I know that at least one number in this list has a square that is greater than 100 okay so let's execute this code and I indeed get the result that at least one number satisfies the condition okay what is the problem here well the problem here is that we see that the for loop obviously runs over all the elements in the list and that's bad, why is that bad well we know that already the second element in the list the 11 has a square 11 squared is 121 so the second element in the list has a square that already fulfills this condition and now if the question is to be careful here the question was is the square of a number in the list greater than so it's sufficient for the condition to be fulfilled if there's only one number it doesn't matter where this number is so in other words the number 11 already fulfills this condition and now I'm still continuing the for loop and that's totally useless right because we already know that the condition is fulfilled so what can we do there is the so called break statement and the break statement helps us or allows us to break out of a loop and how can we use it so here's a re-implementation of the code so I still have my initial assumption that no number is above no numbers of square is above 100 and then I loop over the numbers one by one again and I check the same condition again and then I set is above to true in when I once I find that a number fulfills the condition I set is above to true and then I just write break break is a statement and what break does is whenever we hit break Python leaves the most enclosing or the innermost enclosing loop so there is only one loop here the for loop so once we hit this break here Python stops the for loop and then Python continues to read the code here and here we only check if there is a number that fulfills the condition or not so let's execute this and now I like the program already better so we only loop over two elements and the second element fulfills the condition and because of that the condition is fulfilled for the entire list because that's the question we wanted to solve or to answer and this code is now more efficient however in the worst case scenario if there is not a single element in the list whose square is beyond or larger than 100 then this wouldn't work so let's for example change to 100 to 1,000 so I know that this the condition, the search condition is not fulfilled for a single number and what this means is I still go over all the elements so again when we analyze how a program or how code behaves we are sometimes interested in what is the average performance of an algorithm and sometimes we are interested in what is the worst case performance of an algorithm and the worst case performance of this algorithm still means we have to go over the entire list so this is an example of what is called a linear search and linear search means we are given a list of things or a sequence of things and we have to go in the worst case scenario to the last element in order to find out what we are looking for and there is no faster way of doing this in the worst case so in the worst case we just have to go over all the elements there is nothing that prevents that okay so let's make this code a little bit nicer with the so called optional else clause and now you may wonder why else, we have seen else before it belongs to the compound if statement so what I let me maybe go back, what I don't like about this code here it works it is in a way efficient however I still have like three different sections here I have a section above where I initialize something then I have the actual search logic and then I have some logic at the end that checks if the search was successful or not so the code is kind of like divided into three different sections and this I don't like I want to express the code in a nicer way in a more concise way how do I do that? I use for example the optional else clause how does it look like? well obviously the initializing code up here is gone and now I loop over the numbers and if a single number has a square larger than 100 then I set is above to true and I take out the loop so this part is exactly the same as before and now there is this else clause here and now this is correct so the else clause belongs to the for it does not belong to the if syntactically so syntactically the else belongs to the for and in the else clause I set is above to false and now how do you read this? well you read it like this we loop over the for loop and in a situation where we don't break out of the loop we will go into the else clause however if we break out of the loop we don't go into the else clause so in other words conceptually the if here and the else they still go together so either we hit the condition in the if statement is fulfilled in which case we hit the break statement and we set is above to true but if we never do that then after the loop is over Python executes this one line here and that's the optional else clause not many languages have that but it's something that is very worthwhile to know and unfortunately not many people that work in Python know that this is possible but you see the paradigm here so we are searching for something in a list and if we don't find anything we do something so this search pattern here this is something that we will often do with data so I think it's quite worthwhile to know the optional else clause and again syntactically the else belongs to the for but conceptually the else goes together with the if so we either do this here or we do this here and then afterwards I still have to check if it's above if set to true or false and the code still works as before and it's still as inefficient in the worst case as it was before let's quickly set this to 1000 and it still has to go to the end so it doesn't change anything with the runtime here and now I still want to improve this code I would like to get rid of this last section I want to basically put this somewhere inside the for loop how can I do that well that's quite easy I just do it I just copy paste the prints inside the for loop and I get rid of the is above variable at all so now this is the most concise version of this and if I execute this it's efficient it stops with the first element that fulfills the condition it breaks out and if the condition is not fulfilled we run to the end as before and we don't have three different sections in the code we only have one compound for statement and this in my opinion is not just shorter but this way of formulating the problem also conveys the idea of the problem in a better way so we've learned the break statement and the optional else clause so we they can also be used as I said for the while loop as well and now let's look at something else that also helps us to make for and while loops nicer so here's an example it's about filtering something so here's the example so we are given the same list as always the numbers from 1 to 12 in unordered fashion and the task is to calculate the sum of all even numbers in this list of numbers after squaring them and adding one to the squares so let's kind of translate what this task wants us to do so it says calculate the sum of all numbers in the list and the all implies we have to loop over the list and then it says calculate the sum of all even numbers so even is kind of like it's basically a filter it means that we don't want to do something for all the elements of the list but only for some so we want to filter something out and then at the end of the day we say the individual numbers in this list we don't want to just work with them as they are and add one to them and what is that? Square and add one well this is what a programmer would call a map so we map an x namely the element in the list 2 and y by a function so what is the mapping function here well we squared and we add one so y is f of x and x squared plus 1 that's the mapping and then at the end in the last step for all the remaining numbers after filtering that are mapped we want to sum them up and whenever I whenever I start with many numbers and I want to so called reduce them into one summary statistics so to say then we call this a reduction and this is where the terms filter, map and reduce come from and in other words the so called filter reduce paradigm is a paradigm that is quite often used when we work with big data and my claim is that as business people whenever we work with matrix like data and many data that we work with is matrix like because everything that fits into an axle sheet is at the end of the day matrix data then chances are very high that whatever we want to do with the numbers whatever analysis we want to do with the numbers can be broken down into steps that belong to one of three categories filter, map and reduce and now for now this may seem a little bit too theoretical but you can trust me on that whenever you are able to break your problem whatever you have your concrete problem when you break it down into these three different steps map, filter and reduce then you can be sure today you will find many many standard algorithms that solve the individual steps quite efficiently and also you can most likely scale your solution up to loads of data and so map, filter, reduce paradigm has a little bit to do with parallelized computers parallel computation of things and so on so this is what you need to know in the back of your head when you want to scale a problem up to a big data set okay so let's implement this so we have the same numbers list and here is the code that does all of that and it's very similar to the first example in chapter one so we start here with a total we set it to zero and then we loop over the individual numbers and then we filter out the odd numbers so we have an if statement that says if the total divided by two has no rest so if the number is even then what we do with all the even numbers is we square them and we add one to them and we store that as a square and then I just print it out for debugging so that we see intermediate output and then we take the square and we add it up on the running total so that's exactly the same logic we have done in chapter one in the first example and then when I execute this code what happens is the loop goes over the 12 numbers but only 6 numbers are kept namely the even numbers and then we see how the even numbers 8, 12, 2 and so on how are they mapped to their transformed numbers so 8 is squared to 64 plus 1 gives me 65 and so on and then I add up 65, 145, 37 the 5 also 101, 17 and this leads to 370 ok, that's the example and this is nothing new actually so we've seen this before just with a different task and now let's try to extend this let's try to have a second filter so here's the task again just a little bit modified calculate the sum of every third and even number in this list after square them and adding one to the squares so what do we read here every here still means we have to loop the third, every third number in the list means we have a filter we only keep every third number in the list and then it says every third and even number so even remains so we have two filters now and then we also still square the number and add one and we still sum them up so the last two steps are the same the only thing that's different is we have a second filter let's try to model this in code how can we do that? well first I use the built in enumerate that we just saw before that gives me indexes I wrap it around numbers so now I loop over numbers but in addition to the number I also get an index variable and enumerate by default basis the indexes off of zero but when I give it an optional start argument of one then the first index will be one and this makes sense here because if I want to filter out every third element it's better when I start counting at one here and not at zero so in every iteration of the loop I get a new index and the next number and then I check if the index is divisible by three if so I know that I have a third number so divisible by three means this is the third number, the sixth number the ninth number and the twelfth number these are the numbers that survives the filter that's the first filter and only numbers that pass this filter get processed here so and then we check with a second if statement if the number is also even okay so we can do this and then what happens is we have the same logic but now we have two levels of indentation okay I can execute this and now the eight I look at it's even so I keep it the twelve I look at it's even I keep it the nine I look at but it's not even so I skip it the four I look at it's even and I keep it so it remains eight, twelve and four I square them at one to them and I get 227 so now what's bad with this code what I don't like is what I don't like here is that I indent too much so we have this artificial limit of 80 characters per line that we don't want to skip skip over so by adding more levels of indentation there is a risk that we get closer and closer to this red line here however I would also argue that this code is a bit harder to read because you know I have to understand hey what's going on here what's going on here what's going on here so I have to read from you know in a horizontal way so the code grows horizontally and you know programmers they like code that grows vertically vertically so how can I do that there are two ways the first one is of course I could use an end here the logical end operator and connect the two conditions into one if statement this would work this would save me one level of indentation however this would make the if statement a bit more complex and you know because I'm checking for two different filters I don't want to intermingle the two if statements really if in the real world there are two things then two different conditions that are independent of each other I don't want to have one line of code that you know puts them together I just I try to to model the world as it is and not more complex so if I have two conditions or two filters that I want to be fulfilled I model that with two if statements preferably so what can I do alternatively well Python has the so called continue statement here and the continue statement what it does is basically within a for loop when the for loop hits continue Python immediately goes into the next iteration of the for loop that's all in other words what we do here is I check now for the opposite so I check in this first line of code here first if statement if the number is not number in the list and if it's not a third number then I continue continue meaning I go to the next iteration in the loop I skip it and then in my second in my in the Alice branch here I check if the number is not even so if it's odd and in case I find an odd number I just continue I just skip the next iteration in the loop and then I have my actual code my actual logic that I want to apply the transformation or the mapping as we called it and the reduction and I have it totally unindentured so I have it on the same level of indentation here and this is nice so if I run this code first of all it works as before so I didn't introduce any bug here but then now conceptually it's a lot easier to read because I have all my filters here one by one so now if I asked you to add a third or a fourth or a fifth filter how would you do that add on more ellipses here basically and then it's absolutely clear you have one condition per filter you can nicely document it here or maybe you have some self-explanatory variable and whatever here but then for every filter we have we have one line of code and then we know that if any of these is reached we just skip the element skip the number so this is very easy to read and we know once we're down here when reading this code that when we are here we know the filters are all passed so now we can take care of the mapping and the reduction so I think this code is a little bit nicer and there are companies like Google for example that also enforce coding styles where they don't want their programmers to indent too much so many companies actually so you don't want to indent your code too much and the continuous statement is a nice way of not having to indent a code within a for loop ok let's get to the last section in this chapter and let's see what else can we do with the loops so in particular we can use a while loop as we will see to model something that I call an indefinite loop so what is an indefinite loop an indefinite loop is for example a computer that asks you to enter a number and let's say in a game or so the computer asks you to enter a number and you enter the number and let's say you don't enter a number you enter something invalid and then what should the computer do the computer should keep asking you to tell you ok you entered something wrong please enter a correct you know a correct number or something so it wants to ask you to do something until you do it so what this tells us we know that some code has to be repeated and it has to be repeated until a point where some condition is fulfilled but we don't know what the condition really is the condition lies like the condition is a bit yeah it's random in a way so we know in this game example the loop will eventually end but we don't know when when the user is able to enter something valid to give you an example let's try to implement a game that is called guessing a coin toss so what will happen here is we will model the tossing of a coin and then we will ask our user to enter to make a guess if the coin came up heads or tails and if the guess is correct the game is over and the user won and if the guess is not correct we will throw another coin and the user will guess again so we can already see this game will eventually end but we don't know exactly when so we cannot formulate any condition when this game will end it depends on the user so how would you formulate this well first let's import the random module and then of course for good practice set a random seed and then here is the game the first version so in the first line we say while true so when is the condition here true well true is always true so in other words this condition here leads to an infinite loop so this loop would run forever if we did not break out of it we see already two break statements and I already mentioned that we can use the break statement also within a while loop not only within a for loop so now we know that this loop would run forever unless we hit one of the two break statements so let's analyze this code a little bit further so what's the first line here doing so the first line uses the python's built-in input function and what the input function does it will prompt the user with a text message and allow the user to enter something via the keyboard and whatever comes back from the user is stored in a variable called guess and then we go into the first if statement here so note there are three different if statements an outer one and two nested ones and the outer if statement draws a random number with the random function in the random module and the random function in the random module gives us a number between 0 and 1 a float between 0 and 1 that is uniformly distributed and we compare that to 0.5 in other words in 50% of the cases this condition here the condition that is now in focus will be true in 50% of the cases this condition will not be true and then we say that whenever this condition is true we say we assume that the computer threw hats and in the other case in the else branch we assume that the computer threw tails and then we compare the guests by the user with either the word hats or the word tails and if the user correctly then we will say yes it was hats or yes it was tails and we will break out of the loop if the user however did not guess correctly we will just print the message oops it was and then we will tell the user what it was and then what happens is there is no break statement in those two branches here so what will happen is the loop will continue in the last line but there is no last line which means the flow of execution will start and we will run from the beginning so in other words the user will get a second chance or a third chance so let's run this so here is the prompt and now I can let's say write I want hats I guess hats now and unfortunately the coin toss was tails so let me guess again let's guess hats again and now something happens so I guessed hats and the computer tells me oops it was hats so something must be buggy here so what happened well if we look closely in the lines of code where we check if the gas is equal to hats or if the gas is equal to tails we compare it to the lower case word in other words since python is a case sensitive typing in the gas with an uppercase H for hats in hats will result in bad input so even if we guess correctly we will not win the game so let's write hats in lower case and luckily we win so now we want to improve this code a bit and what don't I like here so two things that I don't like here the first thing is of course there is a bug so somehow we need to allow the user to enter the input with an uppercase H and then the second thing I don't like is there are really two things going on in this problem first the computer tosses a coin and then second the user makes a guess however in the code those two independent steps they are somehow modelled together I don't like this so in a way as I said before if some problem in the real world consists of two independent steps I don't want to see code that indemingles these two steps I don't want my code to be more complex than reality I want my code to be a simpler model or a simple model of the world so let's see how can we improve that remember that in chapter 2 we also talked about how to modularise code and that's exactly what we will do so we will write a function getGas and then the function it has a nice dock string of course but then what does it do well the function that actually reads the dock string so the function processes the user's input and the function takes no arguments and returns either a string or the none type and it says either has for tails if the input can be parsed and none otherwise so this function already takes care and valid so how do we implement it so we have the built-in input function here the built-in input function prompts the user a message and then whatever the user enters will become an object of type string in python and this string is assigned to the variable gas and then what do we do in the second line of code we call methods on the gas on the gas object first we call a method call.strip another method on it called .lower so what does .strip do .strip gets rid of leading and trailing white space so in other words if we prompt the user to enter something and the user accidentally hits the space key first before entering heads for tails then this space will be ignored that's what the .strip does and then .lower does what we think it does it just lower cases all the characters and then whatever we get back from a gas will be stored in gas again and then so now down here once we reach this here we know that we have a text string that is all lower case and so now what we do is we use the in operator that we saw before the container property of the list and check if the gas is in the list of the words heads and tails this contains only two elements the strings, heads or tails and as we saw the in operator just checks if the if the operant on the left hand side is contained in the operant on the right hand side and if it is then we just return it as is because then we know it is either heads or tails so if this condition is true we know that gas is either heads or tails otherwise we return none and the none will mean the user entered something that is invalid something that could not be read as either heads or tails ok so that's the get gas function and now we have another function toss coin and the toss coin function takes an argument called p heads and it defaults to 0.5 so what this means is this function will model at the coin toss however it is parametrized and that's a nice thing because that means we can now by default model a fair coin but by just changing the argument we passed to the toss coin function we can also model an unfair coin and then so how does this function work well the code is rather simple or let's actually check the read the doc string so it takes a probability which is a flow between 0 and 1 we don't validate the input here we assume that p heads is just between 0 and 1 and then it returns at a string either heads or tails so this function now does not return none it always returns heads or tails how is it implemented well if the random number drawn by the random function by the random module is below p heads then it is heads so in other words if we pass in p heads of let's say 0.8 then in 80% of the chances this line the line return heads will be executed and in the other case return tails will be executed so we also use the early exit pattern here because after the first return statement no more line will be executed so this function just gives us back one of two strings and now we have to clue those two functions together how do we do that I use a while true loop so an infinite loop to start with and then I call get guess and I start a result into guess so guess after this first line of code is one of three values it is either heads in lower case or tails in lower case or none and then after that function is executed we execute the next function the toss coin we store this in the variable called result and this will be either lower case heads or lower case tails and now I have three three different cases that I need to take care of so first I check if guess is none please don't be confused that we don't write it like this I don't write it with a double equal this it will work but whenever you compare something to none you should use the is operator the identity operator just accept that and this has to do with the fact that none is a singleton but this is just what we do and then if what is the case is guess is none well we know in this case the user must have entered something invalid in which case we print out a nice message and tell the user hey please enter something correctly and then in the elif branch here we check if guess has the same value as result and this is either true or false and if it is true then we know the user guessed correctly and then we just print out yes it was result because now we know result is whatever the correct result is and then of course we must not forget to break out of the loop and in the other case in the case where the user misgassed the coin toss we just print out oops it was result so let's execute this code cell again and let's for now just enter a text that is invalid enter it and then we get a nice error message saying make sure to enter something correctly and now let's add two spaces here and write hats with a capital H and I get back tails it was tails so let's do it again and hope we guessed correctly it's still not correct and still not correct so three times in a row we now have tails this is something that happens it's quite unlikely so let's try it one more time and now it works so three times the same side of a coin causing a coin several times is a highly unlikely event it just happened I think the code is still correct so it should work and we see that the white space was ignored and tats with an uppercase H does not cause any problems it works ok so we have taken care of both of the downsides the first downside being the problem or the code was buggy and the second one is we had two independent steps in practice and we modeled it with like one intermingled code fragment and here the code is totally isolated so the guessing of the coin so guessing of the coin is totally independent of the tossing of the coin and I like that a lot better because this means my code is not more complex than reality ok this concludes the last slide in this presentation so I briefly summarized what we have learned in this chapter so in this chapter we talked about the different ways of how to implement an iteration logic iteration meaning we execute code repetitively there are two major approaches one is recursion the other one is looping and we see that in memory both of them are a little bit different there are actually cases where recursion can provide a good solution where it is hard to find a good solution with looping generally looping should be the more common case and usually it is enough to loop with the for loop with the for statement but the for statement as we saw is just syntactic sugar for the while statement which allows us to loop to check a condition and loop as long as a condition is true we have seen also cases where both kinds of iteration go wrong so we have seen infinite recursions we have seen infinite loops and then also here we have seen another case which is the indefinite loop and I think the indefinite loop is quite nice because it allows us to code up games and so you will probably in the first couple of examples in your first couple of weeks of coding mostly use the for loop and a little bit of the while loop I think you won't work too much with recursion in the beginning if you want in the exercises you will find a very popular recursion example which is called the Towers of Hanoi and you can look at this and this is a problem which you can only solve by recursion and which is also a classical problem so it's very well studied and you should also look into that and then as you get better in programming, more experienced if you want to solve problems from the field let's say supply chain management and logistics you should learn about a field called dynamic programming which is conceptually similar to what we learned in recursion so we solved a problem from the end in the backwards session and many problems in supply chain management are solved this way so it's definitely worthwhile to know what a recursion is and if you don't get recursion the first time around that is absolutely no problem I think recursion is a topic that is quite challenging to some people but you should if you don't understand it keep on repeating it until you get it because as I said there are nice applications with it and other than that we saw the first time that we have the list object we looked at the concrete list object and tried to analyze some of its abstract behaviors and we said that it has two properties that we identified in this chapter the first property of a list is that we can loop over it and any object that can be looped over for example and then also the second property was that a list object contains other objects and we also saw that also see that of course when we draw a list in memory where a list is nothing but a collection of references to other objects so it truly a list object is nothing but a container for other objects and then we said that these two properties are independent of each other as we will see in chapter 7 then finally list objects have a total of 4 independent abstract concepts behind them and together this will make up the so called idea of a sequence and sequential data is one of the most common ways of data we will look at as business people so this is very worthwhile to study and other than that this is it in chapter 5 we will look at numbers in detail and we will come back to the fourth statement in detail also in chapter 7