Slide 1

Today: String formatting, random numbers, more on doctests


Slide 2

String formatting

We have looked at using the print() function to print out various values: strings, numbers, etc.

We often want to format our printing so that it is readable. So, today we are going to start with string formatting, which enables us to create a string from text, variable, and (as it turns out), any expression we want so that the string ends up being what we want. Example:

def print_ages(current_age, start_year, number_of_years):
    """
    We want to print a list that is formatted like this:
      In 2021, you will be 20 years old.
      In 2022, you will be 21 years old.
      ...

    Doctest:
    >>> print_ages(20, 2021, 3)
    In 2021, you will be 20 years old.
    In 2022, you will be 21 years old.
    In 2023, you will be 22 years old.
    >>>
    """
    for i in range(number_of_years):
        print(...) 

What do we want to replace the ... with above?

There are options! We could do it the "old fashioned way," where we convert the numbers to strings and tie everything together with addition signs (concatenating each piece):

        print('In ' + str(start_year + i) + ', you will be ' + str(current_age + i) + ' years old.')

Well, that is some ugly code. It's kind of hard to read, and all of the interal space formatting is annoying to do. But, it works.

There is a better way! Instead of all of those string conversions and concatenations, we can create a formatted string (also known as "f-strings"), using the letter f before our string. Formatted strings allow you to embed expressions directly into the strings, simply be enclosing them in curly braces ({}). Here is the solution to the problem we posed:

        print(f'In {start_year + i}, you will be {current_age + i} years old.')

Notice a couple of things about the statement above:

  • It reads much nicer. The formatting is easier to write, and the spacing looks nicer.
  • The string starts with f. Without that, the formatting would not occur.
  • There was no need to numbers to strings – this happens automatically
  • The expressions are all enclosed in curly braces.

Here is a nice tutorial on f-strings, and here is a PyCharm project: f-strings.zip


Slide 3

Here are some more examples about string formatting. If you want to actually put a curly brace into a formatted string, you must use two curly braces.

>>> a = 5
>>> b = 6
>>> print(f'The product of {a} and {b} is {a * b}')
The product of 5 and 6 is 30
>>>
>>> # You don't have to use a print statement:
>>> result_str = f'The sum of {a} and {b} is {a + b}'
>>> result_str
'The sum of 5 and 6 is 11'
>>>
>>> # you can add curly braces with two of them:
>>> print(f'This is a curly brace: {{, and so is this: }}')
This is a curly brace: {, and so is this: }
>>> # don't forget the f!
>>> print('The product of {a} and {b} is {a * b}')
The product of {a} and {b} is {a * b}
>>>

Slide 4

Let's move on to a new example and exercise, "movie": movie.zip

The move-starter.py file is the code with bugs, and movie.py is a copy of that to work on, and movie-solution.py has the correct code.


Slide 5

Movie Project + Testing Themes

  • Goal: we want an animation where letters fly leftwards on a black background
    Kind of like The Matrix
  • Divide and Conquer - our mantra
  • Test each function separately - our other mantra
  • Don't debug the animation as it runs
  • Debug a tiny, frozen Doctest case
  • Huge time saver
  • Today: Doctest for a 2d algorithm function

Slide 6

Grid Utility Code

  • We have done lots of 2d algorithms on images
    Always with RGB input/output
    Nice, but just one corner of 2d algorithms
  • Grid - generic 2d facility
  • In the grid.py file
  • 2d storage of str, int, .. anything
  • Reference: Grid Reference

Slide 7

Grid Functions

  • grid = Grid(4, 3) - create, all None initially
  • Zero based x,y coordinates for every square in the grid:
    origin at upper left
    x: 0..grid.width - 1
    y: 0..grid.height - 1
  • grid.width - access width or height
  • grid.get(0, 0) - returns contents at x,y (error if out of bounds)
  • grid.set(0, 0, 'a') - set at x,y
  • grid.in_bounds(2, 2) - returns True if x,y is in bounds

Slide 8

Write Test for set_edges()

  • Let's write a test for the set_edges() function in movie.zip
  • Literal format used to create and check grid values
  • Can set a variable within the >>> Doctest, use on later line
    >>> grid = Grid.build([['b', 'b', 'b'], ['x', 'x', 'x']])
  • Now by typing ctrl-shift-r .. we can test this function in isolation

Here's a visualization - before and after - of grid and how set_edges() modifies it.
alt: set_edges() grid before and
after

Here are the key 3 lines added to set_edges() that make the Doctest: (1) build a "before" grid, (2) call fn with it, (3) write out the expected result of the function call

    ...
    >>> grid = Grid.build([['b', 'b', 'b'], ['x', 'x', 'x']])
    >>> set_edges(grid)
    [['a', 'b', 'a'], ['a', 'x', 'a']]
    ...

Slide 9

Run Doctest in PyCharm

  • Look at set_edges() in PyCharm
  • Select the ">>>" Doctest
    Right click it .. Run Doctest
  • Maybe have to click a 2nd time for PyCharm to recognize the test
  • If the Run Doctest is not present in the menu
    You may need to close PyCharm and open the "movie" folder using the PyCharm open... menu
  • With the set_edges() code correct, the test should pass
  • (optional) Can try putting in a bug


Slide 10

Random Numbers - Random Module

"Anyone who attempts to generate random numbers by deterministic means is, of course, living in a state of sin." John von Neumann

A computer program is "deterministic" - each time you run the lines with the same input, they do exactly the same thing. Creating random numbers is a challenge, so we settle for "pseudo-random" which are statistically random looking, but in fact are generated by an algorithm that selects the series of numbers.

  • A "module" is a library of code we want to use
  • Python has many built-in modules containing useful functions
  • More module information later in the quarter
  • Here the "random" module
  • import random - this line once at the top of your file
  • random.randrange(n) - returns 0..n-1 at random
  • random.choice('string') - returns 1 char at random

Try the "Python Console" tab at the lower-left of your PyCharm window to get an interpreter. It may have a prompt like "In[2]:", but it's basically the same as the old >>> interpreter.

>>> import random   # starter code has this already
>>>
>>> random.randrange(10)
1
>>> random.randrange(10)
3
>>> random.randrange(10)
9
>>> random.randrange(10)
1
>>> random.randrange(10)
8
>>> random.choice('mariposa')
'r'
>>> random.choice('mariposa')
'a'
>>> random.choice('mariposa')
'o'
>>> random.choice('mariposa')
'o'
>>> random.choice('mariposa')
's'
>>> random.choice('mariposa')
's'
>>> random.choice('mariposa')
'm'
>>> random.choice('mariposa')
's'

Slide 11

random_right() Function

The code for this one is provided to fill in letters at the right edge. We're not testing this one - testing random behavior is a pain, although it is possible.

def random_right(grid):
    """
    Set the right edge of the grid to some
    random letters from 'mariposa'.
    (provided)
    """
    for y in range(grid.height):
        if random.randrange(10) == 0:
            char = random.choice('mariposa')
            grid.set(grid.width - 1, y, char)
    return grid

Slide 12

scroll_left(grid)

  • Real algorithmic code of this project
  • Kind of tricky
  • The tests are going to save us here
  • First sketch out the idea without code
  • Then we'll work out the code for it

Slide 13

Scroll Ideas

  • For every x,y
  • Want to "move" the 'd' or whatever one to the left, e.g. x-1
  • Don't move the None if not needed
  • Do not move something to x = -1
    That's out of bounds!

Think about scroll_left()
alt: moving 'd' over for
scroll_left()

Before:

NNN
NdN

After:

NNN
dNN
  • Write some code in scroll_left() - version 1
  • Move square like 'd' to x-1, but only if x > 0
  • Version 1 shown below
  • Has some bugs
  • Run movie.py to see it animate
  • Fun to watch, but not good for debugging

Slide 14

scroll_left() v1 with bugs

def scroll_left(grid):
    """
    Implement scroll_left as in lecture notes.
    """
    # v1 - has bugs
    for y in range(grid.height):
        for x in range(grid.width):
            # Move letter at x,y leftwards
            val = grid.get(x, y)
            if grid.in_bounds(x - 1, y) and val != None:
                grid.set(x - 1, y, val)
    return grid
  • Run this in the full GUI. It's bug, but at least it's funny.
  • Observe: small bugs can create big output effects
    Your output may be totally haywire
    But the bug may just be a -1 somewhere
  • Running the whole program .. not a good way to debug
  • Want: small, isolated, frozen, visible case to look at

Slide 15

Add Doctest before/after

We'll use this as the "before" case:

[['a', 'b', 'c'], ['d', None, None]]

What should the after/result look like for that case? If you are writing the code for something, a good first step is writing out an example "after" case for it. Thinking through before/after pair clearly is a good first step, getting your coding ideas organized.

After:

[['b', 'c', None], [None, None, None]]

Strategy aside: if you need to write an algorithm and are staring at a blank screen. Write out a couple before/after cases to get your thoughts started. It's easier to write tests than the code.


Slide 16

scroll_left() With Doctests

Use this to iterate on the code, get it perfect. The Doctest here is just the one spelled out above.

def scroll_left(grid):
    """
    Implement scroll_left as in lecture notes.
    >>> grid = Grid.build([['a', 'b', 'c'], ['d', None, None]])
    >>> scroll_left(grid)
    [['b', 'c', None], [None, None, None]]
    """

Slide 17

Work on scroll_left()

How do you debug a function? Run its small, frozen Doctests, look at the "got"

  • Bug has to do with blanking out a square copying from
  • Also dealing with x=0
  • After running test once, use ctrl-shift-r (on the mac anyway)
    re-run most recent test .. super common case
  • Look at the got data from the Doctest
  • Then look at your code .. often that's enough
  • Fix the code in lecture
    1. grid.set(x, y, None) within the if\
    2. Move it outside the if
  • Instead of in_bounds(), checking x > 0 would work
    Since we are only worried about going off the left edge

Slide 18

scroll_left() Solution

Here is the code with bugs fixed.

def scroll_left(grid):
    """
    Implement scroll_left as in lecture notes.
    >>> grid = Grid.build([['a', 'b', 'c'], ['d', None, None]])
    >>> scroll_left(grid)
    [['b', 'c', None], [None, None, None]]
    """
    for y in range(grid.height):
        for x in range(grid.width):
            # Move letter at x,y leftwards
            val = grid.get(x, y)
            if grid.in_bounds(x - 1, y) and val != None:
                grid.set(x - 1, y, val)
            grid.set(x, y, None)
    return grid

Slide 19

Run Movie

  • Then run the movie program again
  • With latest movie.zip .. can specify width/height numbers
$ python3 movie.py
$
$ python3 movie.py 80 40  # bigger window

Slide 20

Testing - Strategy Conclusions

  • Run whole program, see a bug
  • Hard to debug with all the code at once
  • Write a test per function
  • Test case:
  • 1. Small
  • 2. Isolated
  • 3. Frozen
  • 4. Visible input/output
    Look at "got" .. if you remember one thing
  • This is the easiest way to get code working
  • We debugged on scroll_left() with its 3x2 case
  • Fixed using the small case, the whole big program worked perfectly