<jupyter_start><jupyter_text>Lecture 5 - Loops (https://bit.ly/intro_python_05)Today:* More control flow:  * For loops  * List iteration  * Range  * Nested loops  * Break statement  * Continue statement* Look at some complex examples of control flow that use control statements to get more comfortable  For Loops The for loop is another example of "syntactic sugar": it combines the concept of looping (like the while loop) with the idea of iterating, either using a counter or elements in a sequence, such as a Python list.You can always create equivalent code using a while loop and additional variables, but 'for' is often more convenientRecall that a while loop encodes: A for loop is like this:It implicitly embeds means to update the variable used in assessing the condition expression.It is easiest to see this by example:<jupyter_code># While loop version
i = 0 # Initialize the "loop variable"

while i < 10: # Conditional on loop variable
  print("i is: " + str(i))
  i = i + 1 # Update loop variable
# The for loop version

for i in range(10): # Range returns an iteratable sequence from 0 (inclusive) to 10 (exclusive)
    print("i is: " + str(i))
# The general syntax is:

for loop_variable in iterable:
  statement_block<jupyter_output><empty_output><jupyter_text>Challenge 1<jupyter_code># Use a for loop to sum the first 100 integers and print the result<jupyter_output><empty_output><jupyter_text>Iterating on lists<jupyter_code>l = [ "this", "is", "a", "list", "of", "strings"] # A list can be iterated upon, because in a for loop it generates an 
# iterable sequence

for i in l:
  print(i)<jupyter_output>this
is
a
list
of
strings<jupyter_text>RangeRange is a powerful "function" for generating iterable sequences of integers.<jupyter_code># If you give range two arguments the first is the starting number of the iteration
# the second is the ending point of the iteration (exclusive), counting up 1 each time:

for i in range(5, 10): # From 5 (inclusive) to 10 (exclusive)
  print("i is: " + str(i))
# If you give range a third argument it determines the "step" of the iteration
for i in range(9, -1, -1): # Backwards from 9 to 0, stepping -1 each time:
  print("i is: " + str(i))
# Jumping two steps at a time
for i in range(0, 10, 2): 
  print("i is: " + str(i))<jupyter_output>i is: 0
i is: 2
i is: 4
i is: 6
i is: 8<jupyter_text>Challenge 2<jupyter_code># Use range and a for loop to print every third integer starting at 20 and ending at and including 38<jupyter_output><empty_output><jupyter_text>Nesting loopsWe saw nested conditionals, we can also nest loops.While loops, for loops and other control flow elements can all be nested together.<jupyter_code># Example of nested for loops

for i in range(4): 
  for j in range(4):  
    print("The values are, i: " + str(i) + " j: " + str(j))
    
# This may mess with you, but it is just the logical extension
# of the ideas we've covered.<jupyter_output>The values are, i: 0 j: 0
The values are, i: 0 j: 1
The values are, i: 0 j: 2
The values are, i: 0 j: 3
The values are, i: 1 j: 0
The values are, i: 1 j: 1
The values are, i: 1 j: 2
The values are, i: 1 j: 3
The values are, i: 2 j: 0
The values are, i: 2 j: 1
The values are, i: 2 j: 2
The values are, i: 2 j: 3
The values are, i: 3 j: 0
The values are, i: 3 j: 1
The values are, i: 3 j: 2
The values are, i: 3 j: 3<jupyter_text>BreakBreak allows us to modify while and for loops to achieve more complex control flow.Think of it as another way to exit a loop:Break evaluates a conditional, if true execution leaves the loop at that point<jupyter_code># Break is used to jump out of a loop

i = 0

while True: # This will always be True
  print(i)
  
  i = i + 1 # Increase i, or the loop will never terminate 
  
  if i > 10:
    break # This causes execution of the loop to stop 
    
print("We are at the end")<jupyter_output>0
1
2
3
4
5
6
7
8
9
10
We are at the end<jupyter_text>Break to shortcut a loopBreak is often used to shortcut a loop, for efficiency reasons<jupyter_code># An example of break to shortcut a loop

# Check if element is in a list

l = [ 10, 1, 12, 7, 8, 2 ] # The list

# Loop through list to see if it contains
# a value less than 5
seen = False
for i in l:
  print("Looking at", i)
  if i < 5:
    seen = True
    break # This causes execution to stop
    
print("is a value less than 5 in the list", seen)<jupyter_output>Looking at 10
Looking at 1
is a value less than 5 in the list True<jupyter_text>Without the break statement the loop would continue to evaluate all the elements on the sequence, but with it we only need to look at elements until the condition is determined to be true  In general, why is break useful?* It can usefully short-cut a loop, preventing unnecessary computation* It allows more fine grain control of loops - you can choose to exit wherever you like  Challenge 3<jupyter_code># Add a conditional and a break statement to exit
# the loop when the user enters a number greater than 0

while True:
    x = float(input("Enter a number greater than 0:"))<jupyter_output><empty_output><jupyter_text>The else statement in loopsElse can be used with while and for loops to execute code after the looping has finished.This is a little obscure, but worth knowing<jupyter_code>for i in range(10):
  print(i)
else: 
  print("The end") # This statement block after the else will
  # be executed at the end of the for loop<jupyter_output>0
1
2
3
4
5
6
7
8
9
The end<jupyter_text>You might ask, why is this useful? Surely it is the same as:<jupyter_code>for i in range(10):
  print(i)
print("The end")<jupyter_output>0
1
2
3
4
5
6
7
8
9
The end<jupyter_text>The answer is that the statement block after the else is only executed if the loop exits "normally", without exiting from a break:<jupyter_code># Using break with the for/else statement

# Check if element is in a list

l = [ 10, 12, 7, 8, 2 ] # The list

# Loop through list to see if it contains
# a value less than 5
seen = False
for i in l:
  print(i)
  if i < 5:
    seen = True
    break # This causes execution to stop
else: # This else is only executed if the 
  # for exits without traversing a break statement
  print("I did not find it!")
    
print(seen)<jupyter_output>10
12
7
8
I did not find it!
False<jupyter_text>Break With Nested Loops Question: How does break behave with nested loops?<jupyter_code>for i in range(1, 6):
    for j in range(1, 3):
        if i + j > 3:
            print("i am going to break", i, j)
            break
        print("The value of i is:", i, "the value of j is:", j, "there sum is:", i+j)
    print("I'm still in the outer loop") # As this value gets printed, even when we break, it
    # is clear break only "breaks out" of the inner most loop it is contained in
print("we're done")<jupyter_output>The value of i is: 1 the value of j is: 1 there sum is: 2
The value of i is: 1 the value of j is: 2 there sum is: 3
I'm still in the outer loop
The value of i is: 2 the value of j is: 1 there sum is: 3
i am going to break 2 2
I'm still in the outer loop
i am going to break 3 1
I'm still in the outer loop
i am going to break 4 1
I'm still in the outer loop
i am going to break 5 1
I'm still in the outer loop
we're done<jupyter_text>Answer: It only breaks out of the inner most loop it is contained in. To break out of two or more loops, consider using a boolean variable:<jupyter_code>for i in range(1, 6):
    breaked = False
    for j in range(1, 3):
        if i + j > 3:
            print("i am going to break", i, j)
            breaked = True
            break
        print("The value of i is:", i, "the value of j is:", j, "there sum is:", i+j)
    if breaked:
        break
    print("I'm still in the outer loop") # As this value gets printed, even when we break, it
    # is clear break only "breaks out" of the inner most loop it is contained in
print("we're done")<jupyter_output>The value of i is: 1 the value of j is: 1 there sum is: 2
The value of i is: 1 the value of j is: 2 there sum is: 3
I'm still in the outer loop
The value of i is: 2 the value of j is: 1 there sum is: 3
i am going to break 2 2
we're done<jupyter_text>ContinueContinue let's us skip the remaining portion of a loop, but not exit the loop itself.<jupyter_code># An example of using continue: sum integers > 5 in a sequence

l = [ 10, 1, 12, 7, 8, 2 ] # The list

j = 0 # Sum of elements in the list

for i in l:
  if i < 5:
    print("skipping " + str(i))

    continue # This says go back to the top of the loop and evalute
    # the loop conditional
  
  print("adding", i)
  j += i # Add the value of i to j
  
print("The sum of elements in the list,"
      + " ignoring values less than 5 is: " + str(j))<jupyter_output>adding 10
skipping 1
adding 12
adding 7
adding 8
skipping 2
The sum of elements in the list, ignoring values less than 5 is: 37<jupyter_text>Continue let's you be more selective about how much of a loop you execute. You can always substitute it for if/else constructs, but often it is more elegant (and less code) to use continue.  Challenge 4<jupyter_code># Add a conditional and continue statement to skip printing any word beginning with the letter 'I', 'l' or 'i'

l = [ "I'm", "learning", "Python", "because", "I'm", "into", "herpetology"]

for i in l:
    print(i)<jupyter_output>I'm
learning
Python
because
I
thought
it
was
herpetology<jupyter_text>Let's practice a bitThe following code examples are designed to make you think a little bit and practice what we've learned.If they doesn't make sense to you try reviewing the material we've covered so far to figure out what you're struggling with.They are all directly adapted from Chapter 7 of the open textbook, so you can use textbook to help you understand them.   Counting digits How many digits, base 10, are there in an integer?<jupyter_code>n = int(input("Enter a whole number: "))
  
# Calculate the number of digits in an integer (base 10).
# e.g. For n = 6706 returns 4
count = 0
while n != 0:
  count = count + 1
  n = n // 10 # This is the integer division operator, it loses the remainder
  
print("Number of digits in input is: ", count)<jupyter_output>Enter a whole number: 56347854378000
Number of digits in input is:  14<jupyter_text>Challenge: Can you see how to adapt the above to count how many digits are in the binary representation of the number? (https://en.wikipedia.org/wiki/Binary_number)  Collatz 3n + 1 sequenceLet's look at the Collatz 3n + 1 sequence:* Start from some given n, the next term in the sequence is either half n if n is even, or else 3*n + 1.  * The sequence continues recursively until it terminates when n reaches 1.The conjecture is that the sequence always reaches 1 (and therefore terminates). This empirically seems to be true: The x axis shows the numbers from 1 to 10 million, the y-axis is the length of there Collatz 3n + 1 sequence.The conjecture remains unproven: https://en.wikipedia.org/wiki/Collatz_conjecture<jupyter_code># Print the 3n+1 sequence from n,
# terminating when it reaches 1.

n = int(input("Enter a whole number: "))

print("The Collatz 3n +1 sequence is: ")
while n != 1:
  print(n, end=" ") # Print the next number in the sequence
  
  if n % 2 == 0:        # n is even
    n = n // 2
  else:                 # n is odd
    n = n * 3 + 1<jupyter_output>Enter a whole number: 31
The Collatz 3n +1 sequence is: 
31 94 47 142 71 214 107 322 161 484 242 121 364 182 91 274 137 412 206 103 310 155 466 233 700 350 175 526 263 790 395 1186 593 1780 890 445 1336 668 334 167 502 251 754 377 1132 566 283 850 425 1276 638 319 958 479 1438 719 2158 1079 3238 1619 4858 2429 7288 3644 1822 911 2734 1367 4102 2051 6154 3077 9232 4616 2308 1154 577 1732 866 433 1300 650 325 976 488 244 122 61 184 92 46 23 70 35 106 53 160 80 40 20 10 5 16 8 4 2<jupyter_text>Challenge: Alter the above to calculate the sum of integers in the Collatz 3n + 1 sequence and print this sum at the end.  A sqrt functionNewton's method to find square roots relies on the observation that if x is a guess of the square root of n then:  * (x + n/x)/2 is a better approximation.(If curious, see: https://math.mit.edu/~stevenj/18.335/newton-sqrt.pdf)For example:Say n = 49 and x = 3 then we can calculate the sequence of approximations iteratively:* x = 3, * x = (3 + 49/3)/2 = 9 2/3, * x = (9 + 2/3 + 49(9 + 2/3))/2 ~= 7.36* x = (7.36 + 49/(7.36))/2 = 7.008* etc...<jupyter_code>n = float(input("Enter a number: "))
x = n/2.0 # Start with guess at the answer
for i in range(10):
    x = (x + n/x)/2
    print("guess is", x) # See the intermediate guesses
print("The sqrt is approximately:", x)<jupyter_output>Enter a number: 64
guess is 17.0
guess is 10.382352941176471
guess is 8.273329445092484
guess is 8.004515049597044
guess is 8.000001273385879
guess is 8.000000000000101
guess is 8.0
guess is 8.0
guess is 8.0
guess is 8.0
The sqrt is approximately: 8.0<jupyter_text>Challenge 5<jupyter_code># Challenge: Adapt the previous example (copied below) 
# to iterate until the absolute difference between successive values of x is less than a 0.00001
# Hint: try using a while loop and break statement and the abs() function

n = float(input("Enter a number: "))
x = n/2.0 # Start with guess at the answer
for i in range(10):
    x = (x + n/x)/2
    print("guess is", x) # See the intermediate guesses
print("The sqrt is approximately:", x)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.07757330939557151, 'fraction_numerical': 0.056029323758228604, 'mean_word_length': 3.6712089447938503, 'pattern_counts': {'":': 1, '<': 70, '<?xml version=': 0, '>': 70, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 1, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '20', 'dir': 'jupyter-structured-clean-dedup', 'id': '366838', 'license': 'MIT', 'n_tokens_mistral': 4900, 'n_tokens_neox': 4189, 'n_words': 2198, 'path': 'lecture_notebooks/L05 Loops.ipynb', 'repo_name': 'chmote/intro_python', 'size': '13368'}