<jupyter_start><jupyter_text>LoopsThe `if` statement is used to conditionally execute some parts of the code. This means that, depending on the value of some **variables**, a part of the code will be executed 0 or 1 times.Another common requirement for a Python program is to repeat some operations multipe times. This is done with **loop** statements.As the `if` statements deal with **boolean** variables, the **loop** statements usually deal with containers like **lists**.  The `for` loopIn the previous lesson you wrote code to perform operations on every element of a **list**; however, this is a very repetitive task: you can do it if your **list** has a small number of elements, but it would become unsustainable in case of hundreds or thousands of elements.Moreover, the code will have to be updated whenever either the number of elements in the **list** or the operation you want to perform change.<jupyter_code>a = [1, 8, 64]

x = a[0]
print(x)
x = a[1]
print(x)
x = a[2]
print(x)<jupyter_output><empty_output><jupyter_text>Here is where the `for` loop statement comes into help.A `for` loop requires a **list** object in order to be used and it allows to write a piece of code that will be executed on each of the elements of the **list**.This procedure is called iteration.An iteration consists in executing the code that is in the body of the loop statement using one of the element in the **list**.The `for` loop goes through all the elements of the **list** one after the other, according to their order (i.e. their **index**).The `for` loop runs as many iterations as the number of elements in the **list**.Note that the whole body is called on one element before proceeding to the next one.<jupyter_code>def divide(z):
    return z / 2

my_list = [1, 8, 64]
for x in my_list:
    print("This iteration is working on", x)
    half_x = divide(x)
    print("The result is", half_x)<jupyter_output><empty_output><jupyter_text>A `for` loop is characterized by: - the `for` keyword - a loop **variable** (`x` in the example above), i.e. a name to be used to refer to the current element in the **list** in turn - the **list** you want to work on - a body that does some work using the loop **variable**You can note the similarities betwen the `for` and the other statements: you need a colon `:` at the end of the first line and the body must be indented.The first line of the `for` statement always defines a so called loop **variable**.This **variable** is a place-holder and it allows to use values from the **list** in the statement body: this works exactly in the same way as input parameters of **functions** which allow to use external values into the **function** body.The first iteration will assign the first element of the **list** to the loop variable and then it will execute the whole body of the `for` loop using that value. Then the successive iteration will start and the successive element in the **list** will be automatically assigned to the loop **variable**. This will continue until all the elements in the **list** have been processed.The name of this **variable** is not relevant, simply be careful to not use a name that is already used by some other **variables** in the code.  ExerciseDefine a **function** that given an input number, returns the number itself if it's positive or `0` if the input number is negative.Use a `for` loop to call the **function** on every value in the input **list** and to print all the returned values.<jupyter_code># Input list
x = [1, 0, -2, -4, 5]<jupyter_output><empty_output><jupyter_text>ExerciseDefine a **function** that takes a **list** as input argument and returns the sum of its elements. Call it on both input **lists**.Hint: you need to use a counter to incrementally keep track of the sum<jupyter_code># Input data
x = [100, 1000, 10]
y = [1, 1]<jupyter_output><empty_output><jupyter_text>How to use `for` loops effectivelyA `for` loop can be used for a wide variety of reasons and most of the times it's required when working with a **list**. Remember that it allows to do an operation multiple times, using all the elements of a **list**.Examples of use-cases are: - To call a **function** on every element of a container. - To update an external **variable** multiple times according to the elements of a container.<jupyter_code>c = 0
for x in [10, 20, 30]:
    c = c + x # Update an external variable
    print("The current elemenet is", x, "the current sum is", c) # Call a function on the current element<jupyter_output><empty_output><jupyter_text>In the above example, the **variable** `c` is defined outside of the `for` loop and then it is used inside.The **variable** is initialized once before the loop and then it is updated in every loop iteration.On the other hand, if you define a **variable** inside a `for` loop, its value will be reset at every iteration.<jupyter_code>for x in [10, 20, 30]:
    # THE FOLLOWING ASSIGNMENT IS WRONG IN THIS EXAMPLE.
    # IF YOU WANT TO SUM VALUES, THE COUNTER VARIABLE MUST BE
    # DEFINED OUTSIDE OF THE FOR LOOP, OTHERWISE IT WILL BE
    # RE-INITIALIZED AT EVRY ITERATION
    c = 0
    c = c + x
    print("The current elemenet is", x, "the current sum is", c)<jupyter_output><empty_output><jupyter_text>The next code block shows something else to **never do**: when your are doing a `for` loop, you are interested in **reading** the values within the **list** through the loop **variable**, so it is never useful to assign a new value to them.<jupyter_code>for x in [10, 20, 30]:
    # THE FOLLOWING ASSIGNMENT IS WRONG, NO MATTER WHAT YOU ASSIGN TO X
    # IF YOU DO A FOR LOOP YOU WANT TO READ THE VALUES IN THE LIST
    # IF YOU ASSIGN A NEW VALUE TO THE LOOP VARIABLE
    # YOU LOSE THE INFORMATION ABOUT THE CURRENT ELEMENT IN THE LIST
    x = 5<jupyter_output><empty_output><jupyter_text>Debugging Python programsIn computer science, to **debug** is the process of fixing errors in a computer program.You can have two main types of errors.Syntax errors consist in writing a line of code that is not recognized by Python. This kind of errors are automatically detected when you run your program and they will result in stopping the execution.<jupyter_code># Missing `,` and closing `]`
a = [1, 2 3<jupyter_output><empty_output><jupyter_text>Functional errors consist in having a Python program that does not do what is supposed to do, even if it does not show any syntax error and it terminates succesfully.<jupyter_code>def max(a, b):
    # It should check `>` instead of `<`
    if a < b:
        return a
    else:
        return b

print(max(10, 20))<jupyter_output><empty_output><jupyter_text>Fixing functional errors is not easy because you have no initial clue about where the error could be.This is particularly true when dealing with `if` and `for` statements as you are required to think about what happens during each iteration of the loop and about which lines are executed in each case.A very naive, but effective, way for understanding what's going on, is to fill your code with `print()` statements. Print every **variable** and every condition that your code works with.This is similar to what you would do in your mind while trying to figure out what the program is doing and why it is not producing the expected output.Examples of what to print are: - In the line before an `if` statement, print all values involved in the condition  - In all the cases of an `if` statement (i.e. `if`, `elif`, etc..), print something to know that a specific case has been executed.Besides allowing you to check if the values make sense, this will allow you to easily see if the correct case is selected.  - In the first line of a `for` loop print the current state of your system, i.e. the current value of the loop variable and eventually also other additional variables. As a rule of thumb, the more you print, the better, as long as you remember to add meaningful messages to the `print()` function rather than just printing plain numbers and then having to figure out which call originated a particular output line.An extremely useful, but often neglected, practice consists in writing the line number where a print statement is located.<jupyter_code>x = 20
y = 10
for a in [22, 11, 33, 55, 44]:
    print("Line 4 ---- Start of iteration with a:", a, "x:", x, "y:", y)
    if a < x:
        x = a - 1
        print("Line 7 ---- a is smaller than x, setting x:", x, "and jump to next iteration")
        continue
    if a > y:
        y = a + 1
        print("Line 11 ---- a is greater than y, setting y:", y)
    print("Line 12 ---- x:", x, "y:", y)
    if x > y:
        y = x + 1
        print("Line 15 ---- x is greater than y, setting y:", y)
    else:
        x = y - 1
        print("Line 18 ---- x is smaller than y, setting x:", x)
    print("Line 19 ---- End of iteration with a:", a, "x:", x, "y:", y)
    
print("#####")
print("#####")
print("#####")
print("final values are x:", x, "y:", y)<jupyter_output><empty_output><jupyter_text>Scope of variables and `for` loopsA **variable** can't be used outside the scope where it is defined. This is usually equivalent to say that a **variable** can only be used in lines with the same or more level of indentation than where it has been defined.The input parameters of **functions** and the loop **variables** have a slightly different rule. - The scope of **function** input parameters is ONLY the **function** body. - A **variable** defined outside a **function**, CAN'T be used in the **function** body. - The scope of loop **variables** is the loop body. Note that accessing a loop **variable** AFTER the loop body will not cause a syntax error, but it will most of the times cause a functional error. It's never required to do that. - A **variable** defined outside a loop, CAN be used in the loop body.A program that does not follow this rule, will never be correct. It may work for some particular input, but it will eventually break.<jupyter_code>def count_greater_than_2(my_list):
    # `my_list` is considered to be defined here
    c = 0 # `c` is defined here, it couldn't be defined outside of the function
    for x in my_list:
        # `x` is considered to be defined here
        if x > 2:
            # loops allow to use variables defined outside
            c = c + 1 # `c` can be used here as this line is contained in its scope

    # `x` can't be used here as this line is outside of its scope
    return c # `c` can be used here as this line is at the same level of its scope
        
# `my_list` can't be used here as this line is outside of its scope
# `c` can't be used here as this line is outside of its scope
print(count_greater_than_2([1, 2, 3, 4, 5]))<jupyter_output><empty_output><jupyter_text>ExerciseWrite a **function** that takes as input a list of positive numbers and returns the highest number in the **list**.Hints:  - You need to use an additional **variable**, similarly to what done with counters. This **variable** will store the highest element found so far. - Think carefully which value to use to initlalize this **variable**.<jupyter_code># Input list
x = [1, 2, 3, 4, 10]
y = [5, 8, 6, 4]
z = [2, 1]<jupyter_output><empty_output><jupyter_text>Controlling the flow of a loopPython supports multiple levels of indentation. This means that you can have a statement inside a statement inside a statement inside a statement...You can combine together statements of different types, i.e. `for` and `if`.<jupyter_code>a = [10, 100, 1000]

for x in a:
    if x == 10:
        print("The value is 10")
    else:
        print("The value is not 10")<jupyter_output><empty_output><jupyter_text>ExerciseDefine a **function** that takes a **list** as input parameter and returns the sum of all the elements with a value between 5 and 9.<jupyter_code># Input lists
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
b = [6]<jupyter_output><empty_output><jupyter_text>More flow controlAnother use case for `if` statements inside `for` loops consists in allowing to skip iterations or to terminate the loop early when some particular condition is satisfied.This is done using the keywords `continue` and `break`. - `continue` ends the current iteration and makes the loop to continue with the next one. - `break` ends the current iteration and terminates the loop, ignoring remaining elements in the container. Note how both keywords stops the current iteration. Similarly to `return` in a function, the lines after a `continue` or `break` are not executed.<jupyter_code>a = [15, 20, 30, 45, 50]

print("Start the loop")

for x in a:
    print("Start of iteration with x =", x)
    if x == 20:
        continue # If this line is executed, stop the current iteration and start the next one
    if x % 2 == 0:
        break # If this line is executed, exit from the loop
    print("End of iteration with x =", x)
    
print("End of the loop")<jupyter_output><empty_output><jupyter_text>The `continue` and `break` keyword **must** always be within an `if` statement.The motivation is similar to when you have multiple `return` in a **function** body: lines after them are not executed, so this has to happen only in some situations, not always.<jupyter_code>a = [10, 20, 30, 40, 50]

# Lines after a `continue` are not executed, so this loop is
# equivalent to one with only the first line
for x in a:
    print("I'm doing something with", x)
    continue
    print("I'm doing something else with", x)
    
print("-----")    

# A `break` stop the execution of the loop.
# This is not really a loop, but it's equivalent to call the first line with the first element of the list
for x in a:
    print("I'm doing such crazy things with", x)
    break
    print("I'm doing bad things with", x)<jupyter_output><empty_output><jupyter_text>ExerciseDefine a **function** that concatenates **string**s until the `"TAG"` symbol is found.Hints: - **String** concatenation is done using the `+` operator with strings: `x = "a" + "bc" + "d"` will result in `x = "abcd"`. - This exercise is almost equivalent to summing all the elements in a list using a counter **variable**, but now the "counter" will be a **string**, not a number. How should you initialize it?<jupyter_code># Input list
sequence = ['CAG','TAC','CAA','TAG','TAC','CAG','CAA']<jupyter_output><empty_output><jupyter_text>ExerciseDefine a **function** that plays Black Jack: it should take a deck as input and "draw" cards from it until the sum of the points of the cards drawn is either 21 or more.If the sum is exactly 21, the game is won, otherwise it's lost.The function should return whether the game was won or not.Play the game using `deck_1`, then, only if the game is won, play the game using also `deck_2`.Hint: use a loop to draw cards from the deck.<jupyter_code># Draw cards in order from the deck
deck_1 = [5, 10, 1, 1, 3, 1, 10, 6, 7, 10]
deck_2 = [7, 9, 7, 2, 5, 8, 1, 1, 5, 9]<jupyter_output><empty_output><jupyter_text>Nested `for` loopsYou can have a `for` loop inside a `for` loop. These are called nested loops.Remember that, for each element in the container, the whole body of a loop is excuted once.This will result in restarting the inner loop in every new iteration of the outer loop.<jupyter_code>for x in [1, 2, 3]:
    print("outer loop:", x)
    for y in [10, 20, 30]:
        print("inner loop:", y, "and outer loop:", x)<jupyter_output><empty_output><jupyter_text>The `continue` and `break` keywords can be used also with nested loops, but keep in mind that they will only apply to the body of the specific loop that is containing them.<jupyter_code>for x in [10, 20, 30, 40]:
    print("outer loop:", x)
    if x == 30:
        continue
    for y in [1, 2, 3, 4]:
        print("inner loop:", y, "and outer loop:", x)
        if x > 10 and y == 2:
            break
    print("end of outer loop:", x)<jupyter_output><empty_output><jupyter_text>ExerciseDefine a **function** that given 2 input numbers returns the smallest.Call this function on every possible pair of X-Y values and sum together all the returned values (i.e. call it on first X and first Y, first X and second Y, first X and third Y...).Hints:  - Use a nested `for` loop to call the **function** on every pair and a counter to keep track of the sum - The result is 38<jupyter_code># Problem data
x_values = [2, 5, 8]
y_values = [4, 19, 4, 1]<jupyter_output><empty_output><jupyter_text>ExerciseWrite a block of code that does the following task:for each `x` value, this has to be multiplied with each of the `y` values.For each `x` value, it should also check if the sum of all these products is greater than the threshold and print a message accordingly.Hints: - Use a nested `for` loop and a counter. - Try to understand the key difference between this execise and the previous one, in particular where the counter should be initialized. - Only the first two `x` values (`4` and `6.28`) will have a sum of products greater than the threshold.<jupyter_code># Problem data
x_values = [4, 6.28, 2, 0.5]
y_values = [0.1, 3, 7, -3.1, 2.6]
threshold = 20<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08992320769095492, 'fraction_numerical': 0.014830881059851106, 'mean_word_length': 4.0, 'pattern_counts': {'":': 0, '<': 92, '<?xml version=': 0, '>': 94, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '22', 'dir': 'jupyter-structured-clean-dedup', 'id': '296095', 'license': 'MIT', 'n_tokens_mistral': 5022, 'n_tokens_neox': 4718, 'n_words': 2644, 'path': '06_loops/loops.ipynb', 'repo_name': 'alsora/python3-course-notebooks', 'size': '17059'}