Today: foreach loop instead of for/i/range, start lists, how to write main()

Q: How To Loop Over Chars in s?

A: for/i/range

for i in range(len(s)):
    # Use s[i]

for/i/range Picture

alt: for i/range, i is 0, 1, 2 .. use s[i[ per char

We've used this many times and we'll continue to.

for/i/range Forms - We've Had a Good Run

So many memories! Looping over string index numbers.

for i in range(len(s)):

Looping over x values in an image

for x in range(image.width):

Looping over y values in an grid:

for y in range(grid.height):

Looping over index numbers like this is an important code pattern and we'll continue to use it.

BUT now we're going to see another way to do it!


Foreach loop: for ch in s:

for ch in s:
    # Use ch

A "foreach" loop can loop over a string and access each char directly. We are not bothering with index numbers, but pointing the variable at each char directly. This is easier than the for/i/range form above. In the loop, the ch variable points to each char, one per iteration:
'H' 'e' 'l' 'l' 'o'

alt: foreach loop, ch points to 'H', 'e', 'l' ...

Foreach Example: double_char2()

> double_char2()

Like earlier double_char(), but using the foreach for/ch/s loop form. The variable ch points to one char for each iteration. The word direct captures this form. The variable points right at the char of interest, vs. indirecting going through the index numbers.

Note: no index numbers, no []

def double_char2(s):
    result = ''
    for ch in s:
        result = result + ch + ch
    return result

How do you know which loop to use?

1. Use simple foreach if you need each char but not its index number. This is the easier option, so we are happy to use it where it's good enough. The double_char2() code above is an example, using each char but not needing index numbers.

2. Use for/i/range if you need each char and also its index number, as shown in the find_alpha() function below.

Example: find_alpha(s)

> find_alpha()

'66abc7' ->  2
'!777'   -> -1
 012345

find_alpha(s): Given a string s, return the index of the first alphabetic char in s, or -1 if there are none.

Q: Do we need index numbers for this? Yes. We need the index number in the loop so we can return it, just having 'a' from '66abc77' is not enough. Need the 2. Therefore use for/i/range.

This is also an example of an early return strategy — return out of the middle of the loop if we find the answer, not doing the rest of the iterations. Then the return after the end of loop is for the did-not-find case.

find_alpha(s) Solution

def find_alpha(s):
    for i in range(len(s)):
        if s[i].isalpha():
            return i  # need index here
    return -1

(optional) Example: sum_digits2()

> sum_digits2()

'12ab3' -> 6
'777' -> 21
'abcd' -> 0

Revisit this problem, now do it with foreach. Recall also str/int conversion.

Q: Do we need the index number of each char to compute this? No, we just need each char. Therefore foreach is sufficient.

sum_digits() Solution

def sum_digits2(s):
    sum = 0
    for ch in s:
        if ch.isdigit():
            num = int(ch)  # '7' -> int 7
            sum += num
    return sum

Python Lists

See the guide: Python List for more details about lists

Python Prides Itself - Elegance, Consistency

Compared to many computer languages, Python does a better job of being clean and consistent. In particular, if code works in one situation, Python tries to make that same syntax work in other situations too.

This feature appears strongly between strings and lists. Many features that work on strings, work the same on lists. In particular, the role of len() and square brackets are the same between the two. When we get to those list features, we'll just point out that lists work the same as strings.

Examples on Server list1

See the "list1" for examples below on the experimental server

1. List Literal: ['aa, 'bb', 'cc', 'dd']
len(lst)

Use square brackets [..] to write a list in code (a "literal" list value), separating elements with commas. Python will print out a list value using this same square bracket format.

The len() function works on lists, same as strings.

The "empty list" is just 2 square brackets with nothing within: []

alt: lst points to list of 'a' 'b' 'c' 'd'

>>> lst = ['aa, 'bb', 'cc', 'dd']
>>> lst
['aa, 'bb', 'cc', 'dd']
>>> 
>>> len(lst)
4
>>> lst = []   # empty list
>>> len(lst)
0
>>>
>>> ['hello', 'hi',]  # trailing comma allowed
['hello', 'hi']

2. Square Brackets to access/change element

Use square brackets to access an element in a list. Valid index numbers are 0..len-1. Unlike string, you can assign = to change an element within the list.

>>> lst = ['aa, 'bb', 'cc', 'dd']
>>> lst[0]
'aa'
>>> lst[2]
'cc'
>>>
>>> lst[0] = 'apple'   # Change elem
>>> lst
['apple', 'bb', 'cc', 'dd']
>>>
>>>
>>> lst[9]
Error:list index out of range
>>>

3. List lst.append('thing')

# Common list-build pattern:
# Make empty list, then call .append() on it
>>> lst = []         
>>> lst.append(10)
>>> lst.append(20)
>>> lst.append(30)
>>> 
>>> lst
[10, 20, 30]
>>> len(lst)
3
>>> lst[0]
10
>>> lst[2]
30
>>>

Example: list_n10()

> list_n10()

list_n10(5) -> [0, 10, 20, 30, 40]

list_n10(n): Given non-negative int n, return a list [0, 10, 20,..(n - 1) * 10]. e.g. n is 4 returns [0, 10, 20, 30]. If n is 0, return the empty list. Use for/i/range and append().

list_n10() Plan

Want:
list_n10(5) -> [0, 10, 20, 30, 40)

Plan:
range(5) -> [0, 1, 2, 3, 4]

Loop over above, multiply each num * 10, .append that

list_n10() Solution

def list_n(n):
    nums = []
    for i in range(n):
        nums.append(i * 10)
    return nums

Experiment: 'nom'

Change the list_n code to append 'nom' in the loop instead of a number. what does the resulting list look like for each value of n? This also shows that the list can as easily contain a bunch of strings as a bunch of ints.

Error #1: lst += something
Muscle Memory

Do not use += on lists for now. Unfortunately += is not the same as .append() for list. We may show what it does later, but for now just avoid it.

# NO does not work
lst += 'thing'


# YES this way
lst.append('thing')

We use += with stings inside a loop for the accumulate pattern, so you have a muscle memory to type it in your code. Sadly it does not work for lists the way you would expect.

Append Error #2: lst = lst.append()

The list.append() function modifies the existing list. It returns None. Therefore the following code pattern will not work, setting lst to None:

# NO does not work
lst = []
lst = lst.append(1)
lst = lst.append(2)


# YES this way
lst = []
lst.append(1)
lst.append(2)
# lst points to [1, 2]

It's easy to get this wrong since we are accustomed to x = change(x) for strings, but that pattern does not work for lists.


Important reminders, how to add onto the end of a thing:

String: s += xxx

List: lst.append(xxx)

String and List work the same 80% of the time. These are tricky, since adding data is one of the cases where the two are different.


4. List "in" / "not in" Tests

>>> lst = ['aa', 'bb', 'cc', 'dd']
>>> 'cc' in lst
True
>>> 'x' in lst
False
>>> 'x' not in lst  # preferred form to check not-in
True
>>> not 'x' in lst  # equivalent, not preferred
True

5. Foreach On List

How to loop over the elements in a list? The foreach loop works.

alt: foreach over list, one elem at a time

>>> lst = ['aa', 'bb', 'cc', 'dd']
>>> for s in lst:
...   # Use s in here
...   print(s)
... 
aa
bb
cc
dd

(do this later) Style - Choose Foreach Var Name

The word after for is the name of a variable for the loop to use. Choose the var name to reflect type of element, s for a string, n for a number. This helps you fill in the loop body correctly.

# list of strings
for s in strs:
  ...

# list numbers
for n in nums:
  ...

# list of urls
for url in urls:
  ...

Example: List intersect(a, b)

> intersect()

intersect([1, 2, 3, 4], [4, 2]) -> [2, 4]

intersect(a, b): Given two lists of numbers, a and b. Construct and return a new list made of all the elements in a which are also in b.

Minor point: since it's a list of numbers, we'll use n as the variable name in the foreach over this list.

List intersect(a, b) Solution

def intersect(a, b):
    result = []
    for n in a:
        if n in b:
            result.append(n)
    return result

Reflect - Why People Like Python

The intersect() code is a good example of what people like about Python — letting us express our algorithmic ideas with minimal fuss. Also shows our preference for using builtin functions, in this case "in", to do the work for us when possible.

6. list.index(target) - Find Index of Target

>>> lst = ['aa', 'bb', 'cc', 'dd']
>>> lst.index('cc')
2
>>> lst.index('x')
ValueError: 'x' is not in list
>>> 'x' in lst
False
>>> 'cc' in lst
True
>>>
>>> # Like this:
>>> # check "in" before .index()
>>> if 'cc' in lst:
      print(lst.index('cc'))
2
>>>

Example: donut_index(foods)

> donut_index()

donut_index(foods): Given "foods" list of food name strings. If 'donut' appears in the list return its int index. Otherwise return -1. No loops, use .index(). The solution is quite short.

e.g. foods = ['apple', 'donut', 'banana'].

Return index in the list of 'donut', or -1 if not found?

donut_index() Solution

def donut_index(foods):
    if 'donut' in foods:
        return foods.index('donut')
    return -1

Optional: list_censor()

> list_censor()

list_censor(n, censor): Given non-negative int n, and a list of "censored" int values, return a list of the form [1, 2, 3, .. n], except omit any numbers which are in the censor list. e.g. n=5 and censor=[1, 3] return [2, 4, 5]. For n=0 return the empty list.

Solution

def list_censor(n, censor):
    nums = []
    for i in range(n):
        # Introduce "num" var since we use it twice
        # Use "in" to check censor
        num = i + 1
        if num not in censor:
            nums.append(num)
    return nums

(skip today) Style Note: Lists and the letter "s"

As your algorithms grow more complicated, with three or four variables running through your code, it can become difficult to keep straight in your mind which variable, say, is the number and which variable is the list of numbers. Many little bugs have the form of wrong-variable mixups like that.

Therefore, an excellent practice is to name your list variables ending with the letter "s", like "nums" above, or "urls" or "weights".

urls = []   # name for list of urls


url = ''    # name for one url

Then when you have an append, or a loop, you can see the singular and plural variables next to each other, reinforcing that you have it right.

    url = (some crazy computation)
    urls.append(url)

Or like this

    for url in urls:

Constants in Python

...
STATES = ['CA, 'NY', 'NV', 'KY', 'OK']
...
...

def some_fn():
    # can use STATES in here
    for state in STATES:
        ... 

ALPHABET Constant in HW4

# provided ALPHABET constant - list of the regular alphabet
# in lowercase. Refer to this simply as ALPHABET in your code.
# This list should not be modified.
ALPHABET = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']

...

def foo():
    for ch in ALPHABET:  # this works
        print(ch)

A Bit of Magic Between Worlds

The command line is its own inscrutable world, and you have typed commands and used the arrow key and whatnot in that world. Then there is the world of Python code inside your programs — def and parameters and strings and loops, and you have spent much time working in that unique world as well.

What is the connection between these two worlds? The code in main() we'll work out today is the connection, so it can seem a bit magic. It is also more simple than you might think.

It's like the sequence is


## 1. Command Line
python3 image-grid.py -channels poppy.jpg

## 2. main() Mystery
??????????

## 3. Python Code
def draw_image(image, out..
    for y in range(image.height):
        ....

Command Line Arguments and main()

You have run your code from the command line many times. The function main() is typically the first function to run in a python program, and its job is looking at the command line arguments and figuring out how to start up the program. With HW4, it's time for you to write your own main(). You might think it requires some advanced CS trickery, but it's easier than you might think.

How Do Command Line Arguments Work?
How to write main() code?

For details see guide: Python main() (uses affirm example too)

affirm.zip Example/exercise of main() command line args. You can do it yourself here, or just watch the examples below to see the ideas.

1. Run affirm.py See Its Arguments

First run affirm.py, see what the command line arguments (aka "args") it takes:
-affirm and -hello options (aka "flags")

$ python3 affirm.py -hello Lisa
Hello Lisa
$ python3 affirm.py -affirm Lisa
Everything is coming up Lisa
$ python3 affirm.py -affirm Bart
Looking good Bart
$ python3 affirm.py -affirm Maggie
Today is the day for Maggie
$

Command Line Argument e.g. -affirm

2. "Command Line Args" of a Program

The command line arguments, or "args", are the extra words typed on the command line, telling the program what to do. The system is deceptively simple - the command line arguments are just the words after the program.py on the command line. So in this command line:

$ python3 affirm.py -affirm Lisa

The words -affirm and Lisa are the 2 command line args. Each arg is 1 word. They are separated from each other by spaces on the command line.

alt: -affirm and Lisa are the args

3. Special Function: main()
Has args Python List

When a Python program starts running, typically the run begins in the function named main(). This function can look at the command line arguments, and figure out what functions to call. (Other computer languages also use this convention - main() is the first to run.)

In our main() code, the variable args is set up as a Python list containing the command line args. Each arg is a string - just what was typed on the command line. So the args list communicates to us what was typed on the command line.

$ python3 affirm.py -affirm Lisa
   ....
   e.g. args == ['-affirm', 'Lisa']


$ python3 affirm.py -hello Bart
    ....
    e.g. args == ['-hello', 'Bart']

4. main()/print() Exercise

Edit the file affirm-exercise.py. In the main() function, find the args = .. line which sets up the args list. Add the following print() - this just prints out the args list so we can see it. We can remove this print() later.

    args = sys.argv[1:]
    
    print('args:', args)   # add this

Now run the program, trying a few different args, so you see that the args list truly just holds whatever words were typed on the command line.

$ python3 affirm-exercise.py -affirm Bart
args: ['-affirm', 'Bart']
$ python3 affirm-exercise.py -affirm Lisa
args: ['-affirm', 'Lisa']
$ python3 affirm-exercise.py -hello Hermione
args: ['-hello', 'Hermione']
$ python3 affirm-exercise.py -hello Python is best
args: ['-hello', 'Python', 'is', 'best']

Q: For the Hermione line: what is args[0]? what is args[1]?

5. print_affirm(name) Helpers Provided

For this exercise, we have helper functions already defined that do the various printouts:

print_affirm(name) - prints affirmation for name
print_hello(name) - print hello to name
print_n_copies(n, name) - given int n,
                          print n copies of name

The code for these is in the affirm.py file and they are not very complicated. The file also defines AFFIRMATIONS as a constant, the random affirmation strings to use.

AFFIRMATIONS = [
    'Looking good',
    'All hail',
    'Horray for',
    'Today is the day for',
    'I have a good feeling about',
    'A big round of applause for',
    'Everything is coming up',
]

def print_affirm(name):
    """
    Given name, print a random affirmation for that name.
    """
    affirmation = random.choice(AFFIRMATIONS)
    print(affirmation, name)

Exercise 1: Make -affirm Work

We'll do this one in lecture.

Make the following command line work, editing the file affirm-exercise.py:

$ python3 affirm-exercise.py -affirm Lisa

Challenge Detect: -affirm Lisa

Detect that the user has typed a command of the form '-affirm Lisa' and call the print_affirm() function with the right name, like this:


# have args list
# e.g. ['-affirm', 'Lisa']

if ????:
    print_affirm(???)

The first challenge is the if-test. What if-test detects that the command line is:
'-affirm Lisa'

1. The number of args is: 2

2. The first arg is: '-affirm'

Then call print_affirm() with the name, extracted from the args, e.g. 'Lisa'. The earlier print(args) helps show what args list contains for each run, so that's a way to visualize the data going into our if-test.

Solution code - 2 lines of dense code. This follows the "lego bricks" theory of coding - Python features like [0] and == we have seen before, here we put them together in a new way to solve a new problem.

def main():
    args = sys.argv[1:]
    ....
    ....
    # 1. Detect command line: -affirm Lisa
    if len(args) == 2 and args[0] == '-affirm':
        print_affirm(args[1])

Exercise 2: Make -hello Work

Write if-logic in main() to detect the following command line form, calling print_hello(name), passing in the correct string.

$ python3 affirm-exercise.py -hello Bart

Solution code

    if len(args) == 2 and args[0] == '-hello':
        print_hello(args[1])

Exercise 3: Make -n Work

In this case, the function to call is print_n_copies(n, name), where n is an int parameter, and name is the name to print. Note that the command line args are all strings, so there is a little issue there.

$ python3 affirm-exercise.py -n 10000 Hermione
Hermione Hermione Hermione Hermione Hermione Hermione Hermione Hermione Hermione Hermione Hermione Hermione Hermione Hermione Hermione ...