Today - big picture divide and conquer, black box examples, boolean logic, string introduction, string +=, string for/i/range loop

Big Picture - Program and Functions

We want to build this program, and we can think of it as a mess of code.

program without decomp - tons of code

Divide and Conquer - Functions

The one key strategy for CS is Divide and Conquer. The first form of this is dividing the program up into individual functions, like this.

program of functions

For today .. focus on what one functions looks like. Especially how data gets in and out of a function, and ultimately we can fit all the functions together. Many bits of Python technology we will see are in service of this program-of-functions strategy.

Strategy: One Function At a Time
Functions are Independent, Sealed Off

For Divide-and-Conquer strategy to work, we want to work on one function at a time. To do that, we need the functions to be independent of each other. When working on a function, we're just looking at its code, not having to think about lines from other functions.

one function at a time


"Black Box" Function, "Functional"

function is black box with input/parameters in and output/return out

We will characterize each function by its inputs and outputs. This is a useful model for thinking about functions. It's a simple way to think about one function, and it provides a way to mesh the functions together to solve the whole program.

Imagine the walls of the function are nice and thick — so it only interacts with its environment through its well defined inputs and outputs. This design keeps the functions independent, but with just enough input/output to interact with the other functions.

In Python, a function's input is its parameters, and its output is from the Python return directive.

Fix Phone Example

Say we are working on an app where people type in phone numbers, like 555-1212.

Suppose that the users make lots of typing mistakes with extra spaces and dashes. We'll have a function fix_phone() that takes in a possibly malformed phone number as an input, and returns a cleaned up phone number as output, with the digits in the right places and one dash.

Think of it as a black box function with an input and output, that would look like this:

alt

Repeatable

The function computes its result using only its input data. As a result, if we call the function twice with the same inputs, we will produce the same output twice. We say that the function is "repeatable".

Cases - Table of Input Output

To think about fix_phone(), think about all the different inputs it should handle, and what the output should be for each one. We call these the "Cases" for a function — a table of inputs, and for each input, what should be the output.

Here we'll say that if an input number does not have 7 digits, then it is unfixable, and in that case the output should be the special value None, signaling that that input was beyond repair.

alt

Cases - Systematic Approach to a Function

Functions can be very abstract. Cases are a more concrete way to think about a function — what inputs to handle, and what the output should be for each one. We might use the word systematic to describe this approach, breaking the problem into a series of cases, making sure that each one is handled.

Cases - A Way To Start the Code

You are staring at the blinking cursor - how to get started writing a function? You can think of one case, and writ code to solve that one case — a nice way to get that first bit of code written.


Now we'll starting coding up many black-box input/output functions, starting in the logic 1 section on the experimental server.

1. winnings1() Example

> winnings1()

def winnings1(score):
    ...
score -> winnings
0        0    # * 10
1        10   # * 10
2        20   # * 10
3        30   # * 10
4        40   # * 10
5        60   # * 12 starts here
6        72   # * 12
7        84   # * 12
8        96   # * 12
9        108  # * 12
10       120  # * 12

Think about this as a black-box function, inputs and outputs.

1. Function Input: Parameter

def winnings1(score):
    ...

2. Function Output: return result

def winnings1(score):
    return score * 10

Experiment 1 - winnings1()

You can type these in and hit the Run button as we go or just watch as we go. The results are pretty easy to follow.

def winnings1(score):
    return score * 10

Experimental Server Output Format

alt: experimental server output table

if-return Pick-Off Strategy

if input == case1:
    return result-for-case1

Experiment 2

def winnings1(score):
    if score < 5:
        return score * 10

Add if-logic to pick off the < 5 case. Run it. We can see that it's right for half the cases.

None Result

Experiment 3 - Very Close

def winnings1(score):
    if score < 5:
        return score * 10

    if score >= 5:
        return score * 12

Add pick-off code for the score >= 5 case (>= means greater-or-equal). This code returns the right result in all cases. There is just at tiny logical flaw.

Experiment 4 - Perfect

def winnings1(score):
    if score < 5:
        return score * 10
    
    # Run gets here: we know score >= 5
    return score * 12

What About This Code?

What about this solution...

def winnings1(score):
    if score < 5:
        return score * 10
        return score * 12

The above does not work. The second return is in the control of the if because of the indentation. It never runs because the line above exits the function. Obviously indentation is very significant for what code means in Python.

Cases - Systematic Programming


2. (optional) Winnings2

Similar practice example on your own.

> winnings2()

Say there are 3 cases. Use a series of if-return to pick them off. Need to think about the order. Need to put the == 10 case early.

Winnings2: The int score number is in the range 0..10 inclusive. Bonus! if score is 10 exactly, winnings is score * 15. If score is between 4 and 9 inclusive, winnings is score * 12. if score is 3 or less, winnings is score * 10. Given int score, compute and return the winnings.

Solution

def winnings2(score):
    if score == 10:
        return score * 15
  
    if score >= 4:  # score <= 9 is implicit
        return score * 12
    
    # All score >= 4 cases have been picked off.
    # so score < 4 here.
    return score * 10

    # Here the cases are handled from 10 to 0.
    # The opposite order would be fine too.

Boolean Values

For more detail, see guide Python Boolean

Produce a Boolean - 1. Equality ==

>>> 5 == 6
False
>>> 5 == 5
True
>>> 5 != 6
True
>>> 'hi' == 'yo'  # works for any type
False
>>>

Produce a Boolean - 2. Ordering

Can try these in the interpreter (Interpreter)

>>> 5 < 10
True
>>> 5 < 5   # strict
False
>>> 
>>> 5 <= 5  # less-or-equal
True
>>> 
>>> 5 > 3
True
>>>
>>> 3.14 < 5.1          # float
True
>>>
>>> 'apple' < 'banana'  # uses alphabetical order
True
>>> 'zebra' > 'book'
True
>>> 

Boolean Operators: and or not

Weather Examples - and or not

Say we have a variable temp which is the temperature, and is_raining is a Boolean indicating if it's raining or not.

# Have variables temp and is_raining with some values
temp = 72
is_raining = False

Q: We want to stay inside if it's under 50 degrees or it's raining. What is the if-test for this?

if temp < 50 or is_raining:
    print('staying inside!')

Q: We'll say it's snowing if the temperature is under 32 and it's raining. What is the if-test for snowing?

if temp < 32 and is_raining:
    print('yay snow!')

Q: What is the if test to detect that the temp is over 70 and it's not raining?

if temp > 70 and not is_raining:
    print('Sunny and nice!')

Style: Do Not Write xxx == True

In an if or while test, do not add == True or == False to the test - though the English phrasing of it does tend to include those words.

Do not write it this way:

if is_raining == True:   # NO not like this
    print('raining')

Write it this way:

if is_raining:           # YES like this
    print('raining')

Another example:

if is_raining == False:     # NO
    print('yay')

Write it this way:

if not is_raining:          # YES
    print('yay')

The if/while machinery checks the if-test for True on its own.

3. is_teen(n) Example

> is_teen()

is_teen(n): Given an int n which is 0 or more. Return True if n is a "teenager", in the range 13..19 inclusive. Otherwise return False. Use the pick-off strategy, detect that the n parameter is teen.

Solution

def is_teen(n):
    # Use and to check
    if n >= 13 and n <= 19:
        return True

    return False

    # Future topic: possible to write this
    # as one-liner: return n >= 13 and n <= 19

Note: Direct English Translation Does Not Work

Tempting write this for problem like is_teen(n), sort of following the way you would speak it:

    if n >= 13 and <= 10:
    ...

This does not work. To work correctly, the and must combine two things which are boolean expressions already, so write it this way:

    if n >= 13 and n <= 19:
    ...

Note how the expressions on either side, e.g. n >= 13, are True/False booleans already, with the and just combining them.

4. (optional) Lottery Scratcher Example

> scratcher()

A similar example for practice.

Lottery scratcher game: We have three icons called a, b, and c. Each is an int in the range 0..10 inclusive. If all three are the same, winnings is 100. Otherwise if 2 are the same, winnings is 50. Otherwise winnings is 0. Given a, b and c inputs, compute and return the winnings. Use and/or/== to make the tests.

Solution:

def scratcher(a, b, c):
    # 1. All 3 the same
    if a == b and b == c:
        return 100
    
    # 2. Pair the same (3 same picked off above)
    if a == b or b == c or a == c:
        return 50
    
    # 3. Run gets to here, nothing matched
    return 0

Strings

For more detail, see guide Python Strings

len() function

>>> len('Hello')
5
>>> s = 'Hello'   # Equivalently
>>> len(s)
5
>>> len('x')
1
>>> len('')
0

Empty String ''

Zero Based Indexing - Super Common

alt: the string 'Python' with 6 index numbers 0..5

String Square Bracket - s[1]

>>> s = 'Python'
>>> s[0]
'P'
>>> s[1]
'y'
>>> s[4]
'o'
>>> s[5]
'n'
>>> s[6]
IndexError: string index out of range

String Immutable

>>> s = 'Python'
>>> s[0]        # read ok
'P'
>>> s[0] = 'X'  # write not ok
TypeError: 'str' object does not support item assignment

String +

>>> a = 'Hello'
>>> b = 'there'
>>> a + b
'Hellothere'
>>> a
'Hello'

1. Set Var With =

We've already used = to set a variable to point to a value. The code below sets s to point to the string 'hello'.

s = 'hello'

alt: set s to point to 'hello'

2. Change Var = "Now Point To"

If we use = to set an existing variable to point to a new value, this just changes the variable to point to the latest value, forgetting the previous value. The assignment = could be translated to English as "now points to".

s = 'hello'
# change s to point to 'bye'
s = 'bye'

alt: change s to point to new string

3. Key Pattern: s = s + something

>>> s = 'hello'
>>> s = s + '!'
>>> # Q: What is s now?

alt: change s to point to new string

Answer: s is 'hello!' after the two lines. So s = s + xxx is a way of adding something to the right side of a string. The following form does the exactly the same thing using += as a shorthand:

>>> s = 'hello'
>>> s += '!'

String += Strategy

Use += to shorten each line. Write a series of these, each one adding on to the end of the string.

>>> s = 'hello'
>>> s += '!'
>>> s += '!'
>>> s
'hello!!'

alt: change s to point to new string

This += form will be a common pattern to add on to the end of a string.


How To Loop Over Chars of a String?

How to write a loop to look at every char? Super common to do this.

1. Look at String Index Numbers

alt: 'Python' want index numbers 0..5

2. Generate Index Numbers With range(len(s))

Generate index numbers with range():

range(len(s)) -> [0, 1, 2, 3, 4, 5]

3. Standard for/i/range loop:

This has lots of syntax, but it is idiomatic and we will use it many times.

for i in range(len(s))
    # use s[i]

This is the standard, idiomatic loop to go through all the index numbers of a string. It's traditional to use a loop variable with the simple name i with this loop. Inside the loop, use s[i] to access each char of the string.

alt: for loop i through the index numbers of s

double_char() Example

double_char(s): Given string s. Return a new string that has 2 chars for every char in s. So 'Hello' returns 'HHeelllloo'. Use a for/i/range loop.

> double_char

Solution code

def double_char(s):
    result = ''
    for i in range(len(s)):
        result = result + s[i] + s[i]
    return result

Exercise: add_star(s)

If we have time, students do one. Look at each char, like double_char().

> add_star

'xyz' -> '*x*y*z'

add_star(s): Given a string s. Return a new string where every char in the original string has a '*' added before it. So 'xyz' returns 'xy*z'. Use a for/i/range loop.

Also, see the experimental server section string2 for many problems like double_char()


(optional/later) print() In The Loop

The experimental server has a feature to give a little insight about what's going on inside the for/i/range loop.

For the double_char code inside the loop: print(i, s[i])

> double_char

def double_char(s):
    result = ''
    for i in range(len(s)):
        print(i, s[i])
        result = result + s[i] + s[i]
    return result

This will print one line for each iteration of the loop, showing what i and s[i] are as the loop runs.

Recall that the print() output is separate from the formal return output of the function. In the experimental server, the print output is shown below the function result, looking like this for the 'xyz' input:

alt: double char output with print

print(i, s[i]) - Think About Output

Say the input is 'xyz'

Q: How many iterations will the loop run?

A: 3 - it runs through the index numbers 0, 1, 2

Q: What lines will print(i, s[i]) produce?

A:

0 x
1 y
2 z