Today: string char class, more string loops, if/else, Doctests, grid, peeps example

Strategy - Re-use Code Patterns

When you write a function, it often resembles a previous function you got working. It's a fine strategy to pull elements from existing, working code to make up new functions. Code often has common, repeated phrases, so we are happy to take advantage of that.

There is a formal "design patterns" strategy in CS, but here we refer to the informal and effective practice of pulling phrases from working code we have laying around.

Recall: double_char() Example

> double_char

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

double_char() Patterns

The double_char() function demonstrates 2 common code phrases, and we'll re-use these in future problems.

1. Look At Every Char

Looking at every char in a string: for i in range(len(s))

alt: for loop i through the index numbers of s

2. Build Result String With +=

result = ''     # Initialize result as empty string

...

result += aaa   # += add to end of result
result += bbb

...

return result   # Return built-up result at end

We'll re-use those code phrases to solve other string problems below.


String Character Class Tests

alt: divide chars into alpha (lower/upper), digit, space, and misc leftovers

s.isalpha() - True for alphabetic word char, i.e. 'a-z' and 'A-Z'. Python uses "unicode" to support many alphabets, e.g. 'Ω' is another alphabetic char.

s.isdigit() - True for digit chars '0' '1' .. '9'

s.isspace() - True for whitespace chars, e.g. space, tab, newline

>>> 'a'.isalpha()
True
>>> 'Z'.isalpha()
True
>>> '4'.isalpha()
False
>>> '$'.isalpha()
False
>>> 'abc'.isalpha()   # Works for multiple chars too
True
>>> '9'.isdigit()
True
>>> 't'.isdigit()
False
>>> ' '.isspace()
True
>>> '\n'.isspace()
True
>>>
>>> 'Ω'.isalpha()     # Greek Omega char (unicode)
True
>>> '水'.isalpha()    # Mandarin "water" char (unicode)
True
>>> 'Ω'.isdigit()
False
>>> '🔥'.isdigit()    # Unicode has emoji "chars"
False
>>>

(exercise) alpha_only(s)

> alpha_only

'H4ip$' -> 'Hip'
'12ab34ba' -> 'abba'

Hint: Loop to look at each char in the string. Milestone: first write a version that just adds every char to the result. Then add if-logic with s[i].isalpha() to put only the alphabetic chars on the result.

Aside: handy lecture music during coding exercises - Creative Commons (CC) licensed music.

double_char() Solution

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

alpha_only() Solution

def alpha_only(s):
    result = ''
    
    # Loop over all index numbers
    for i in range(len(s)):

        # Add s[i] if it's alphabetic
        if s[i].isalpha():
            result += s[i]
    
    return result

Reminder: Regular if

Just as a reminder, here is the regular if-statement we've used many times.

if test-expression:
  lines

Two possibilities: (1) test True: run lines, (2) test False: skip the lines

This simple if-statement is the one to use 90% of the time.

Variation: if / else

See guide for more if/else details: Python-if

Adding the else: clause to the if-statement:

if test-expression:
  lines-1  # run if test True
else:
  lines-2  # run if test False

Two possibilities: (1) test True: run lines-1, (2) test False: run lines-2

Example: str_dx()

> str_dx

'1a24$' -> 'dxddx'

str_dx() Solution

def str_dx(s):
    result = ''
    for i in range(len(s)):
        if s[i].isdigit():
            result += 'd'
        else:
            result += 'x'
    return result

else vs. not

Suppose if some_test is False, you wan to call do_something(). Sometimes people sort of back into using else to do that, like the following, but this is not the best way:

# NO not the best
if some_test:
     pass  # do nothing here
else:
     do_something()

The correct way to do that is with not:

# YES this way
if not some_test:
    do_something()

Early-Return Strategy

The double_char() code needs to loop through the whole input string, and then the last line of the function calls return.

BUT sometimes an algorithm can figure out an answer earlier, calling return earlier to exit the function with an answer without running the lines below. This is form of the pick-off strategy where sometimes we figure out the answer early.

has_alpha() Logic

You know at some level that the interior of the computer is very logical. This problem embodies that theme of very neat, sharp logic.

Consider this function:

> has_alpha

has_alpha(s): Given string s. Return True if there is an alphabetic char within s. If there is no alphabetic char within s, return False.

'3$abc' -> True
'42$@@' -> False

We might say this algorithm is like the book Are You My Mother?

has_alpha() Cases

Think about solving this problem. Look through the chars of the string. When do you know the answer?

alt: look at every char to find alpha

Logic strategy: look at each char. (1) If we see an alpha char, return True immediately. We do not need to look at any more chars. (2) If we look at every char, and never see an alpha char, must conclude that there are no alpha chars and the result should be False. The (1) code goes in the loop. The (2) code goes after the loop - a sort of "by exhaustion" strategy.

has_alpha() Solution

def has_alpha(s):
    for i in range(len(s)):
        if s[i].isalpha():
            # 1. Return True immediately if find
            # alpha char - we're done.
            return True
    
    # 2. If we get to here, there was no
    # alpha char, so return False.
    return False

Big Picture - Program, Functions, Testing

Big Picture - program made of many functions. Want to build out the functions efficiently, concentrating on one function at a time.

alt: program made of functions, each with tests

How Do You Know A Function is Correct?

Bad news: You can't tell if a function is correct by looking at it.

Function Test With Cases - Great in Practice

Good news: It's pretty easy to have a few input/output tests for a function. Tests don't prove 100% that the function is correct, but in practice they work very well.

We're going to show you have to run and write your own function tests in Python. Python is very advanced for this - tests are easy to write, easy to run.

Look at Output ("got") Debugging

Functions can be tested by looking at output (aka "got") for each input — helpfully systematic, use for debugging below.

Old Days

In the old days, you could write separate functions, but it was hard to test them one at a time. You would end up testing the functions all together, and so you would have bugs in function a() and at the same time bugs in b(). The output would be a mixture of both bugs interfering with each other. It could be a real time-wasting disaster. Now we can test each function on its own. Much better!

Tests - Work One Function at a Time

str1 project

Aside: Fix for "No Interpreter"

If it says "no interpreter" at the lower right of PyCharm window. Click it. Select a recent Python version, such as 3.12 or 3.13 (anything recent is fine). If they are not in the list, click Add Local Interpreter. Then System Interpreter, and select a python3 version. Any recent python3 version is fine. On my laptop, the most recent has the cryptic name "/usr/bin/python3"

digits_only(s) Function

We'll use this function to see how tests work.

Given string s, return a string of its digit chars.

'Hi4x3' -> '43'

Python Function Doctests

Doctests are the Python technology for easily testing a function.

For more details, see the Doctest chapter in the guide

digits_only(s)

def digits_only(s):
    """
    Given a string s.
    Return a string made of all
    the chars in s which are digits,
    so 'Hi4!x3' returns '43'.
    Use a for/i/range loop.
    (this code is complete)
    >>> digits_only('Hi4!x3')
    '43'
    >>> digits_only('123')
    '123'
    >>> digits_only('xyz')
    ''
    >>> digits_only('')
    ''
    """
    result = ''
    for i in range(len(s)):
        if s[i].isdigit():
            result += s[i]
    return result

Python Function - Doctest

Here are the key lines that make one Doctest:

    >>> digits_only('Hi4!x3')
    '43'

Doctest Command Line

Usually we run Doctests from within PyCharm. It's also possible to run from the command line like this:

$ python3 -m doctest str1.py 

The output is nothing if the tests pass, otherwise it lists all the failures. This runs all the tests in the file.

Workflow: Functions with Tests

We'll use Doctests to drive the examples in section and on homework-3.


Grid - Peeps Example

Today's grid example peeps.zip

Grid - 2D Algorithms

Grid Functions

Grid Example Code

grid = Grid(3, 2)
grid.width # returns 3
grid.set(2, 0, 'a')
grid.set(2, 1, 'b')

grid.get(2, 0)  -> 'a'
grid.in_bounds(2, 0) -> True
grid.in_bounds(3, 0) -> False

alt: grid, width 3 height 2, 'a' upper right, 'b' lower right

Grid of Peeps

Suppose we have a 2-d grid of peeps candy bunnies. A square in the grid is either 'p' if it contains a peep, or is None if empty.

alt: grid of peeps

Q: Does This x, y have a Happy Peep?

Rule: We'll say an x, y has a happy peep, if that square contains a peep and there is another peep immediately to its left or right.

Look at the grid squares again. For each x, y .. is that a happy peep x, y?

alt: grid of peeps with happy True/False per square

x, y happy?
(top row)
0, 0 -> False (no peep there)
1, 0 -> True
2, 0 -> True

(2nd row, nobody happy)
0, 1 -> False
1, 1 -> False
2, 1 -> False

Want: Doctests for is_happy(grid, x, y)

Grid Square Bracket Syntax

alt: grid of peeps

Here is the syntax for the above grid. The first [ .. ] is the first row, the second [ .. ] is the second row. This is fine for writing the data of a small grid, which is good enough for writing a test.

grid = Grid.build([[None, 'p', 'p'], ['p', None, 'p']])

is_happy() Step 1 - Doctests

alt: grid of peeps

def is_happy(grid, x, y):
    """
    >>> grid = Grid.build([[None, 'p', 'p'], ['p', None, 'p']])
    >>> is_happy(grid, 0, 0)
    False
    >>> is_happy(grid, 1, 0)
    True
    >>> is_happy(grid, 2, 0)
    True
    >>> is_happy(grid, 0, 1)
    False
    >>> is_happy(grid, 2, 1)
    False
    """
    pass

Checking 5 representative squares. Removed """doc words""" so tests and drawing fit on screen at once.

Write is_happy() Code

This is important example, showing the best-practice workflow for building complex code.

We have tests and no code. Run the tests, see the test failures. Use if/pick-off strategy to add in code to fix problems prompted by the test failures.

Workflow — Run, look at the top failed case. Look at the wrong output and the code that produced it to think of your next step. Easier to do this than staring at a blank screen. Also we are more confident it's right when we're done, since we do have a range of tests.

is_happy() Code v1

This code is fine. Using the "pick-off strategy, looking for cases to return True. Then return False as the bottom if none of the cases found another peep.

def is_happy(grid, x, y):
    """
    Given a grid of peeps and in bounds x,y.
    Return True if there is a peep at that x,y
    and it is happy.
    A peep is happy if there is another peep
    immediately to its left or right.
    >>> grid = Grid.build([[None, 'p', 'p'], ['p', None, 'p']])
    >>> is_happy(grid, 0, 0)
    False
    >>> is_happy(grid, 1, 0)
    True
    >>> is_happy(grid, 2, 0)
    True
    >>> is_happy(grid, 0, 1)
    False
    >>> is_happy(grid, 2, 1)
    False
    """
    # 1. Check if there's a peep at x,y
    # If not we can return False immediately.
    if grid.get(x, y) != 'p':
        return False

    # 2. Happy because of peep to right?
    # Must check that x+1 is in bounds before calling get()
    if grid.in_bounds(x + 1, y):
        if grid.get(x + 1, y) == 'p':
            return True

    # 3. Similarly, is there a peep to the left?
    if grid.in_bounds(x - 1, y):
        if grid.get(x - 1, y) == 'p':
            return True

    # 4. If we get to here, not a happy peep,
    # so return False
    return False

is_happy() Using and

The in_bounds() checks can be done with and instead nesting 2 ifs. This works because the "and" works through its tests left-to-right, and stops as soon as it gets a False. This code is a little shorter, but both approaches are fine.

    # 2. Happy because of peep to right?
    # here using "and" instead of 2 ifs
    if grid.in_bounds(x + 1, y) and grid.get(x + 1, y) == 'p':
        return True

Example - has_happy()

(Do this if we have time.)

Say we want to know for one column in the grid, is there a happy peep in there? A column in the grid is identified by its x value - e.g. x == 2 is one column in the grid.

Below is the def for this. The parameters are grid and x - the x identifies the column to check. The return True/False strategy is similar to the one seen in the has_alpha() example above. Doctests are provided, write the code to make it work.

def has_happy(grid, x):
    """
    Given grid of peeps and an in-bounds x.
    Return True if there is a happy peep in
    that column somewhere, or False if there
    is no happy peep.
    >>> grid = Grid.build([[None, 'p', 'p'], ['p', None, 'p']])
    >>> has_happy(grid, 0)
    False
    >>> has_happy(grid, 1)
    True
    """
    # your code here
    pass

has_happy() Solution

def has_happy(grid, x):
    """
    Given grid of peeps and an in-bounds x.
    Return True if there is a happy peep in
    that column somewhere, or False if there
    is no happy peep.
    >>> grid = Grid.build([[None, 'p', 'p'], ['p', None, 'p']])
    >>> has_happy(grid, 0)
    False
    >>> has_happy(grid, 1)
    True
    """
    # x is specified in parameter, loop over all y
    for y in range(grid.height):
        if is_happy(grid, x, y):
            return True
    # if we get here .. no happy peep found
    return False

Doctest Strategy

We're just starting down this path with Doctests. Doctests enable writing little tests for each black-box function as you go, which turns out to be big productivity booster. We will play with this in section and on homework-3.