<jupyter_start><jupyter_text>Program Flow in Python          Table of Contents- [What is a computer Program](def)- [Functions](F)    * [Why Functions?](WF)    * [Syntax](S)- [Conditional & Logic](C&L)- [Iteration](I)    * [Aside (Recursion)](AR)- [Puting it all together](PIAT)- [More about Functions](MAF)- [Some topics we haven't discussed, but have used](last)  What is a computer Program[Go back to the main page](main)    At its simplest a program is a list of instructions that a computer carries out in order. A program could be long and complicated, but it is built of simple parts. Let's look at some simple operations in python, and think about what the computer does for each one.<jupyter_code>1 + 1
2 * 4.4
1 + 1
2 * 4.4<jupyter_output><empty_output><jupyter_text>In the first cell we compute `1 + 1`, and Python returns the results `2`. We can think of this as a very sort program. Similarly, in the second cell we compute `2 * 4.4`, and Python returns the results `8.8`.However, In the third cell, when we combine these to statements as squential lines, we only see Python return `8.8`.This is because, Python can only return one result at the end of the cell, so the first line is evaluated, but we never se the result. One way can report intermdiate results is using `print`.<jupyter_code>print(1 + 1)
print(2 * 4.4)<jupyter_output>2
8.8<jupyter_text>we can also includes **comments** in our lines of code which computer will not execute.These lines can be used to add notes and explanations for the code. We use `` to start a comment in our code.<jupyter_code># this is a comment and it will not be executed
# print 1 + 1
print(f"1 + 1 is {1+1}")
# print 2 * 4.4
print(f"2 * 4.4 is {2*4.4}")
# print 'completed'
print('completed')<jupyter_output>1 + 1 is 2
2 * 4.4 is 8.8
completed<jupyter_text>While writing a code often we not only want to print intermediate results, but *store* them for future use in the code. We can store a result in computer's memory by assigning it to a **variable**.<jupyter_code>first = 1 + 1
final = first * 4.4

print(final)<jupyter_output>8.8<jupyter_text>Here we are able to use the result of the first calculation (stored in `first`) in our second calculation. We store second calculation in `final`, which ww print in third expression at the end of the cell.Variables help us to keep track of the information we need to successfully excute a program. Variables can be used to store a variety of information.<jupyter_code>my_name = 'Vishwas'
my_age = 22
my_favourite_number = 34
has_pet = False

print(f"My name is {my_name}")
print(f"My age is {my_age}")
print(f"My favourite number is {my_favourite_number}")
print(f"Do I have a Pet: {has_pet}")<jupyter_output>My name is Vishwas
My age is 22
My favourite number is 34
Do I have a Pet: False<jupyter_text>Since variables are used to store information, it's good idea to give them discriptive names. This increase code readibility, which helps in fixing mistakes, or colloboration ith others<jupyter_code>print(type(my_name))
print(type(my_age))
print(type(my_favourite_number))
print(type(has_pet))<jupyter_output><class 'str'>
<class 'int'>
<class 'int'>
<class 'bool'><jupyter_text>* A **String** is a sequence of characters.* An **integer** is same as mathematics (i.e. "negative & positive whole numbers"* A **float** or **floating point number** are the decimal numbers (i.e. "real numbers" in mathematics). It is called float because the decimal point is alloed to "float" through the digits, allowing us to rpresent both big numbers (eg. 112334.112) and small numbers (eg. 0.000045).* A **bool** or **boolean** refers to a variable that is either True or False.These are some of the few data we will encounter whiach are mainly used during programing.In Python, we can assign any type of data to a variable without declaring what type the variable will be in advance.   Functions[Go back to the main page](main) Here is a mathematical function defined by $$ f(x) = 3x - 5 $$we give function an input and gets an output$$ f(6)-> 13 $$if we have a variable $$ y = 3 $$and we evaluate function on y we get same result $$ f(y)-> 4 $$ Many programs react to user input. Function allows us to dfine a task a computer will carry out based on a given input. A simply Pyhton function look like this:<jupyter_code>def cube(number):
    return number**3<jupyter_output><empty_output><jupyter_text>We define function using the `def`. Then comes the name of the function, hich in this case is `cube`. we then enclose the function input in paranthesis, in this case `number`. We use `:` to indicate the Python the starting of the body of the functionIn this case th body of the function is vary simple, we return the cube of `number` (we use `**` for exponents in Python). The keyword `return` signals that the function will generate some output. Not every function will have a `return` statement. A `return` statement ends a function<jupyter_code># we can store the function output in variables
cube_6 = cube(6)

print(cube_6)

my_input = 3.4
# we can also use variables as function input
print(cube(my_input))<jupyter_output>216
39.303999999999995<jupyter_text>We can pass different input to the `cube` function, including variables. When we passed a float to `cube`, float is returned. When we passed an integer, `cube` returned integer. In both cases the input was interpretd by the function as the  argument `number`Not all possible inputs are valid.<jupyter_code>print(cube('apple'))<jupyter_output><empty_output><jupyter_text>We ran into an error because `apple` is a string, not a number. This is because exponent doesn't work on `string`. W should be careful t omake sure that the input for a function makes sense for the function's purpose We can test the function using assertion weather the oupput is equal to the given value<jupyter_code>assert cube(3) == 23<jupyter_output><empty_output><jupyter_text>Why Functions?[Go back to the main page](main) On other use of function apart from handling the user input is performing an action multiple times on different input. If we want to perform square a bunch of numbers, in particular the number between 1 to 9, I can do this pretty easily (later we will learn about iteration which will make this even easier!)<jupyter_code>1**2
2**2
3**2
4**2
5**2
6**2
7**2
8**2
9**2
print(1**2)
print(2**2)
print(3**2)
print(4**2)
print(5**2)
print(6**2)
print(7**2)
print(8**2)
print(9**2)<jupyter_output>1
4
9
16
25
36
49
64
81<jupyter_text>What if we want to go back and add `3` to all the results? Clearly we can see the problem here. Lets instead define a function to do our work<jupyter_code>def my_func(x):
    print(x**2 + 3)<jupyter_output><empty_output><jupyter_text>Now we can just call the function on every element. If  want to change the output, we just need to change the function<jupyter_code>my_func(1)
my_func(2)
my_func(3)
my_func(4)
my_func(5)
my_func(6)
my_func(7)
my_func(8)
my_func(9)<jupyter_output>4
7
12
19
28
39
52
67
84<jupyter_text>Splitting out the work into function is often a way to make code more modular and understandable. It also helps ensure your cod is correct. If write a function and test it correct, we kno it will be correct every time we use it. if we don't break out code into a function, it is very easy to make typos or other errors which will cause our progrmas to break   Syntax[Go back to the main page](main) As our instructions to the computer become more complicated, we will need to organize them in a way the computer understands. We've already seen an example of `cube` function. There is a specific order to the words and specific symbols we had to use to let Python know which part of the function we the defination and hich part is the body, or which part is the name of the function and which part is th argument. We call the rules for organizing code the programming language's **Syntax**Python' syntax is very streamlined so that code is readable and intutive. Python uses White space to organize code. For Example:<jupyter_code>def ex_1():
    print('ex_1 is running')
    print('returning value "a"')
    return 'a'

ex_1()
def ex_2():
    print('ex_2 is running')
    print('exiting without returing a value')
    
ex_2()<jupyter_output>ex_2 is running
exiting without returing a value<jupyter_text>The function `ex_1` ends with a return statement, but `ex_2` has no return statement. So how does Python know where `ex_2` ends? We use indentation to indicate which lines are part of th function and which aren't. The indented lines are all grouped together undr the function defination.  Conditional & Logic[Go back to the main page](main) We'll often want the computer only to take an action under certain situations. For Example, we might want a game to print the message 'High Score!', but only if the player's high score is higher than the previous high score. We can write this as a formal logical statement: if the player's score is higher than the previous high scor thn print 'High Score!'The syntax for exprssing this logic in python is vry similar. Let's define a function that accepts the player's score and the previous high score as arguments. If the player's score is higher, then it will print 'High score!'. Finally, it will return the new high score (whichever one that is).<jupyter_code>def test_high_score(player_score, high_score):
    if player_score > high_score:
        print('High score!')
        high_score = player_score
        
    return high_score
print(test_high_score(83, 98))
print(test_high_score(95, 93))<jupyter_output>High score!
95<jupyter_text>With if statements we use a similar syntax as we used for organizing functions. With functions we had a `def` statement ending with `:`, and an indented body. Similarly for a conditional, we have an if statement ending with `:`, and an indented body.Conditional statements are used to control program flow. We can visualize our example, `test_high_score`, in a decision tree![programflow_highscore.png](https://raw.githubusercontent.com/VishwasSaini32/Data-Science/master/Python/images/programflow_highscore.png)We can nest `if` statements to make more complicatd trees<jupyter_code>def nested_example(x):
    if x < 50:
        if x % 2 == 0:
            return 'branch_a'
        else:
            return 'branch_b'
    else:
        return 'branch_c'
    
print(nested_example(42))
print(nested_example(51))
print(nested_example(37))<jupyter_output>branch_a
branch_c
branch_b<jupyter_text>In this example, we have an `if` statement nested under another `if` statement. As we change the input, we end up on different branches of the tree.![programflow_nestedlogic.png](https://raw.githubusercontent.com/VishwasSaini32/Data-Science/master/Python/images/programflow_nestedlogic.png)The statement that follows the `if` is called the **condition**. Th condition can be either true or false. If the condition is true, then we execute the statements under the `if`.If the condition is false, then we execute the statements under the `else` (or if there is no `else`, then we do nothing).Conditions themselves are instructions that Python can interpret.<jupyter_code>print(50 > 10)
print(2 + 2 == 4)
print(-3 > 2)<jupyter_output>True
True
False<jupyter_text>Conditions are evaluated as booleans, which are `True` or `False`. We can combine conditions by asking of condition A _and_ condition B are true. We could also ask if condition A *or* condition B are true. Let's consider whether such statements are true overall based on the possible values of condition A and condition B.|Condition A|Condition B|Condition A and Condition B|Condition A or Condition B||:---------:|:---------:|:-------------------------:|:------------------------:||True|True|True|True||True|False|False|True||False|True|False|True||False|False|False|False|<jupyter_code>print(True and True)
print(True and False)
print(False and True)
print(False and False)
print(True or True)
print(True or False)
print(False or True)
print(False or False)
x = 5
y = 3

print(x > 4 and y > 2)
print(x > 7 and y > 2)
print(x > 7 or y > 2)<jupyter_output>True
False
True<jupyter_text>The keywords `or` and `and` are called **logical operations** (in the same sense that we call `+`, `-`, `*`, etc. arithmetic operations). The last logical operation is `not`: `not True` is `False`, `not False` is `True`<jupyter_code>print(not True)
print(not False)
x = 10
y = 8

print(x > 7 or y < 7)
print(not x > 7 or y < 7)
print(not x > 7 or not y < 7)
print(not (x > 7 or y < 7))<jupyter_output>True
False
True
False<jupyter_text>Iteration[Go back to the main page](main) Conditions are very useful because theey allows our programs to make dcisions based on some information. These decision control the flow of the program (i.e. which statments get executed). We have on other major tool for controlling program flow, which is repetition. In programming, we will use repetitive loops to execute the same code many times. This is called `iteration`. The most basic kind of interation is the `while` loop. A `while` loop will keep executing so long as the condition after the `while` is `True`<jupyter_code>x = 0
while x < 5:
    print(x)
    x = x + 1<jupyter_output>0
1
2
3
4<jupyter_text>We will often use interation to prform a task a certain numbers of times, but we might also use it to carry out a process to a certain stage of completition.As an example of these different cases, we'll considr the Fibonacci sequence.The Fibonacci numbers are the sequence of numbers $\{F_n\}_{n=1}^\infty$ defined by the linear recurrence equation$$ \begin{equation}F_n=F_{n-1}+F_{n-2}\end{equation}$$with $F_1=F_2=1$. As a result of the above definition, it is conventional to define $F_0=0$.The Fibonacci numbers for $n=1, 2, ...$ are $1, 1, 2, 3, 5, 8, 13, 21, ...$ (OEIS [A000045](https://oeis.org/A000045)).![Fibonacci numbers](https://mathworld.wolfram.com/images/gifs/FoxTrot20051011.jpg)The above cartoon (Amend 2005) shows an unconventional sports application of the Fibonacci numbers (left two panels). (The right panel instead applies the [Perrin sequence](https://mathworld.wolfram.com/PerrinSequence.html)).Below we define two functions to compute part of the Fibonacci sequence; the first function computes the first `n` terms, while the second function computes all the terms less than an upper limit, `x`.<jupyter_code>def first_n_fibonacci(n):
    prev_num = 0
    curr_num = 1
    count = 2
    
    print(prev_num)
    print(curr_num)
    
    while count <= n:
        next_num = curr_num + prev_num
        print(next_num)
        prev_num = curr_num
        curr_num = next_num
        count += 1

def below_x_fibonacci(x):
    prev_num = 0
    curr_num = 1
    
    if curr_num < x:
        print(prev_num)
        print(curr_num)
    elif prev_num < x:
        print(prev_num)
        
    while curr_num + prev_num < x:
        next_num = curr_num + prev_num
        print(next_num)
        prev_num = curr_num
        curr_num = next_num
m = 7
print(f'First {m} Fibonacci numbers')
first_n_fibonacci(m)
y = 40
print(f'Fibonacci numbers below {y}')
below_x_fibonacci(y)<jupyter_output>Fibonacci numbers below 40
0
1
1
2
3
5
8
13
21
34<jupyter_text>Sometimes we will want our program to take a repeated action, but we won't know how many repetitions we will have to do, or it might be difficult to know ahead of time when the program should stop. For example, we could write a program that prints out cooking instructions. We don't know in advance how many instructions there will be in the recipe (some meals take a long time to cook and have many steps, while others are short and simple to make). We also don't know what the last instruction might be, so it would be difficult to write a conditional telling the program when to stop. How are we going to solve the problem? Let's look at an example.Instructions for making bread:  1) Dissolve salt in water  2) Mix yeast into water  3) Mix water with flour to form dough  4) Knead dough  5) Let dough rise  6) Shape dough  7) Bake  The recipe has an ordered `list` of instructions. In Python we can use a list of strings to represent the instructions.<jupyter_code>bread_recipe = ['Dissolve salt in water', 'Mix yeast into water', 'Mix water with flour to form dough', 
                'Knead dough', 'Let dough rise', 'Shape dough', 'Bake']<jupyter_output><empty_output><jupyter_text>We could store different recipes in different lists.<jupyter_code>soup_recipe = ['Dissolve salt in water', 'Boil  water', 'Add bones to boiling water', 'Chop onions', 
               'Chop garlic', 'Chop carrot', 'Chop celery', 'Remove bones from water', 
               'Add vegetables to boiling water', 'Add meat to boiling water']

beans_recipe = ['Soak beans in water', 'Dissolve salt in water', 'Heat water and beans to boil', 
                'Drain beans when done cooking']<jupyter_output><empty_output><jupyter_text>Each of these lists has different instructions, and they are not all the same length. The beans recipe has four stps while the soup rcipe has ten. It ould b hard to write a `while` loop to print out each step. It is much easier to do it using a `for` loop.A `for` loop does an action for each item in a `list` (or more precisely, in an **iterable**)<jupyter_code>def print_recipe(instructions):
    count = 1
    for step in instructions:
        print(f'{count}. {step}')
        count += 1
print_recipe(soup_recipe)
print_recipe(bread_recipe)
print_recipe(beans_recipe)<jupyter_output>1. Soak beans in water
2. Dissolve salt in water
3. Heat water and beans to boil
4. Drain beans when done cooking<jupyter_text>We can also use a `for` loop to repeat a task a certain number of times, like printing out the first `n` numbers in the Fibonacci sequence. Compare these two Fibonacci functions<jupyter_code>def first_n_fibonacci_while(n):
    prev_num = 0
    curr_num = 1
    count = 2
    
    print(prev_num)
    print(curr_num)
    
    while count <= n:
        next_num = curr_num + prev_num
        print(next_num)
        prev_num = curr_num
        curr_num = next_num
        count += 1

def first_n_fibonacci_for(n):
    prev_num = 0
    curr_num = 1
        
    print(prev_num)
    print(curr_num)
    
    for count in range(2, n+1):
        next_num = curr_num + prev_num
        print(next_num)
        prev_num = curr_num
        curr_num = next_num
first_n_fibonacci_while(7)
first_n_fibonacci_for(7)<jupyter_output>0
1
1
2
3
5
8
13<jupyter_text>Aside (Recursion)[Go back to the main page](main)Another way to get something like interation is called *recursion*which is when ew define a function in terms of itself. Lets write the Fibonacci sequence recursively. This will be slightly different that it will only calculate the nth Fibonacci number<jupyter_code>def fibonacci_recursive(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fibonacci_recursive(n-1) + fibonacci_recursive(n-2)
fibonacci_recursive(4)<jupyter_output><empty_output><jupyter_text>Here we make use of the fact that a Fibonacci number $F_n$ can be defined in terms of $F_{N-1}$ and $F_{N-2}$ with some base cases $F_0=0$ and $F_1=1$.  We will not be using recursion in this course, but it is an interesting and useful programming construct.  Puting it all together[Go back to the main page](main)We've learned two of the major components of programs: **variable** and **functions**. We've also learned two of the major components of program control: **conditionals** (`if` statements) and **itrations** (`for` and `while` loops). We can use thse ideas and tools to write code to prform complex tasks. Let's look at an example, involving all of these ideas put together.Below we write a function that points out all the prime numbers up to some number `n`. We will use iteration to check if each number is prime. We will use a conditional to print out numbers only if they are prime. We will also break up the task ito small pieces so our code is easy to read and understandable. This means we will use (or call) helper functions inside of our solution.<jupyter_code>def is_prime(number):
    if number <= 1:
        return False
    
    for factor in range(2, number):
        if number % factor == 0:
            return False
    
    return True

def print_prime(n):
    for number in range(1, n):
        if is_prime(number):
            print(f'{number} is prime')
print_prime(10)<jupyter_output>2 is prime
3 is prime
5 is prime
7 is prime<jupyter_text>The other application of the functions might be to do something many times (not necessarily in an iteration). One specific and natural way to understand this is to have a list elements and apply a function of the `list`. Lets take a list of first 20 numbers and find which one are prime. We will do this and save the result in a `list`. Lists have an `append` method which allows us to add an element to the end of the list.<jupyter_code>list_of_numbers = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
prime_list = []
for number in list_of_numbers:
    if is_prime(number):
        prime_list.append(number)
prime_list<jupyter_output><empty_output><jupyter_text>Python provides a nice construct to apply a function to every element of a list, called a `list comprehension`, here is an example of one:<jupyter_code>[number for number in list_of_numbers if is_prime(number)]<jupyter_output><empty_output><jupyter_text>Note that this is a simple bit of code that is very undrstandable. We don't ned to care **how** the `is_prime` computation os occuring, only that its occuring for every element of `list_of_numbers`. This means that we can view our program at a high level without caring about the small details (which hopefully we have already designed well and tested)  More about Functions[Go back to the main page](main)Notice that the `ex_1` and `ex_2` has no input, but other functions like `test_high_score` had multiple variables as input. Remember that a function argument is just a placeholder for a name and will b bound to hatever is passed into the function. For Example<jupyter_code>def print_this(a):
    print(f'inside print_this: {a}')

a = 5
print_this(2)
print(f'a = {a}')<jupyter_output>inside print_this: 2
a = 5<jupyter_text>Notice that even though `print_this` was printing the variable `a` inside the function and there was a variable `a` defined outside of the function, the `print` function inside `print_this` still printed what was passed in.  However, I can also<jupyter_code>def print_it():
    print(f'inside print_it: {a}')
    
a = 5
print_it()
print('a = ', a)<jupyter_output>inside print_it: 5
a =  5<jupyter_text>Here there is no variable passed into the function so python uses th variable from the outer scope. Be careful with this second paradigm as it can be dangerous. The danger lies in th fact that the output of the function depends upon th overall state of th program (namely th value of `a`) as opposed to `print_this` hich depends only on the input of the function. Functions like `print_this` ar much asier to reason about, test, and use, they should be prferred in many contexts.That said, there is a very powerful technique called `function closure` which we can make use of this ability. Lets say we want a function which will raise a number to some exponent, but we don't know which exponent ahead of runtime.<jupyter_code>def some_exponent(exponent):
    def func(x):
        return x**exponent
    return func
some_exponent(2)(2), some_exponent(3)(2)<jupyter_output><empty_output><jupyter_text>Now that we understand how normal arguments work, lets look at a few conveniences Python provides for making functions easier to create. The first is default arguments. Let's suppose we have a function which had a bunch of arguments, but most of them had sane defaults, for example:<jupyter_code>def print_todo(watch_tv, read, eat, sleep):
    print('I need to:')
    if watch_tv:
        print('  watch_tv')
    if read:
        print('  read')
    if eat:
        print('  eat')
    if sleep:
        print('  sleep')
print_todo(True, True, True, True)<jupyter_output>I need to:
  watch_tv
  read
  eat
  sleep<jupyter_text>I know that I almost always need to eat and sleep, so I can use a default argument for these instead.  This means I don't need to define the value of `eat` and `sleep` unless they are different than the default.<jupyter_code>def print_todo_default(watch_tv, read, eat=True, sleep=True):
    print('I need to:')
    if watch_tv:
        print('  watch_tv')
    if read:
        print('  read')
    if eat:
        print('  eat')
    if sleep:
        print('  sleep')
print_todo_default(True, True)<jupyter_output>I need to:
  watch_tv
  read
  eat
  sleep<jupyter_text>These default arguments can allow us to create complex function with many inputs while also maintaining ease of use by setting sane defaults.Another thing we might want to do is take a variable list of arguments, lets write a similar `todo` function as before, but this time we will allow it to pass in any number of argumnts. Hre we will make use of the `*args` syntax. This `*` tells python to gather th rest of the arguments into the tuple `args`.<jupyter_code>def print_todo_args(*args):
    print('I need to:')
    for arg in args:
        print(f'{arg}')
        
print_todo_args('watch_tv', 'read', 'eat', 'sleep')<jupyter_output>I need to:
watch_tv
read
eat
sleep
{'fraction_non_alphanumeric': 0.08181567871612319, 'fraction_numerical': 0.017228493883491326, 'mean_word_length': 4.245306375077368, 'pattern_counts': {'":': 0, '<': 133, '<?xml version=': 0, '>': 136, 'https://': 5, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 1, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '35', 'dir': 'jupyter-structured-clean-dedup', 'id': '411957', 'license': 'CC0-1.0', 'n_tokens_mistral': 7902, 'n_tokens_neox': 7516, 'n_words': 3504, 'path': 'Python/python-programflow.ipynb', 'repo_name': 'VishwasSaini32/Data-Science', 'size': '25423'}