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 heightgrid.get(0, 0)
- returns contents at x,y (error if out of bounds)grid.set(0, 0, 'a')
- set at x,ygrid.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.
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 filerandom.randrange(n)
- returns 0..n-1 at randomrandom.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()
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
- grid.set(x, y, None) within the if\
- 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