July 13th, 2021
Today: parsing, while loop vs. for loop, parse words out of string patterns, boolean precedence, variables
Here's some fun looking data...
$GPGGA,005328.000,3726.1389,N,12210.2515,W,2,07,1.3,22.5,M,-25.7,M,2.0,0000*70 $GPGSA,M,3,09,23,07,16,30,03,27,,,,,,2.3,1.3,1.9*38 $GPRMC,005328.000,A,3726.1389,N,12210.2515,W,0.00,256.18,221217,,,D*78 $GPGGA,005329.000,3726.1389,N,12210.2515,W,2,07,1.3,22.5,M,-25.7,M,2.0,0000*71 $GPGSA,M,3,09,23,07,16,30,03,27,,,,,,2.3,1.3,1.9*38 $GPRMC,005329.000,A,3726.1389,N,12210.2515,W,0.00,256.18,221217,,,D*79 $GPGGA,005330.000,3726.1389,N,12210.2515,W,2,07,1.3,22.5,M,-25.7,M,3.0,0000*78 $GPGSA,M,3,09,23,07,16,30,03,27,,,,,,2.3,1.3,1.9*38 ...
var += 1
'xx @abc x' 012345678
for i/range
vs. while
The for/i/range form is great for going through numbers which you know ahead of time - a common pattern in real programs. However, while is more flexible - can test on each iteration, stop at the right spot. Ultimately you need both forms.
Use for/i/range if have a series of numbers to step through. That is a common case, and for/i/range is perfect for it. We'll use while for situations that require more flexibility.
Here is the while-equivalent to for i in range(n)
i = 0 # 1. init while i < n: # 2. test # use i i += 1 # 3. update, loop-bottom (easy to forget)
> while_double() (in parse1 section)
double_char() written as a while (using a range() is the better approach for this problem, so here just demonstrating for-while equivalence).
def while_double(s): result = '' i = 0 while i < len(s): result += s[i] + s[i] i += 1 return result
Test is i < len(s)
- this is basically "is i valid". We'l use this definition of valid index later.
> at_word() (in parse1 section)
'xx @abc xyz' -> 'abc'
at_word(s): We'll say an at-word is an '@' followed by zero or more alphabetic chars. Find and return the alphabetic part of the first at-word in s, or the empty string if there is none. So 'xx @abc xyz' returns 'abc'.
First use s.find()
to locate the '@'
. Then start end pointing to the right of the '@'
. Use a while loop to advance end over the alphabetic chars. Make a drawing below to sketch out this strategy. Think about the while-test.
at = s.find('@') if at == -1: return '' end = at + 1 # Advance end over alpha chars while s[end].isalpha(): end += 1
'xx @abcd xx'
end = at + 1 while s[end].isalpha(): end += 1
Start of loop:
End of loop:
Once we have at/end computed, pulling out the result word is just a slice.
word = s[at + 1:end] return word
That code is pretty good, but there is actually a bug in the while-loop. It has to do with particular form of input case below, where the alphabetic chars go right up to the end of the string. Think about how the loop works when advancing "end" for the case below.
at = s.find('@') end = at + 1 while s[end].isalpha(): end += 1
'xx@woot' 01234567
Problem: keep advancing "end" .. past the end of the string, eventually end is 7. Then the while-test s[end].isalpha()
throws an error since index 7 is past the end of the string.
The loop above translates to: "advance end so long as s[end] is alphabetic"
To fix the bug, we modify the test to: "advance end so long as s[end] is valid and alphabetic".
In other words, stop advancing if end reaches the end of the string.
Loop end bug:
end < len(s)
Guard TestWe cannot access s[end]
when end is too big. Add a guard test end < len(s)
before the s[end]
. This stops the loop when end gets to 7. The slice then works as before. This code is correct.
def at_word(s): at = s.find('@') if at == -1: return '' # Advance end over alpha chars end = at + 1 while end < len(s) and s[end].isalpha(): end += 1 word = s[at + 1:end] return word
The "and" evaluates left to right. As soon as it sees a False
it stops. In this way the < len(s)
guard checks that "end" is a valid number, before s[end]
tries to use it. This a standard pattern: the index-is-valid guard is first, then "and", then s[end]
that uses the index. We'll see more examples of this guard pattern.
exclamation(s): We'll say an exclamation is zero or more alphabetic chars ending with a '!'. Find and return the first exclamation in s, or the empty string if there is none. So 'xx hi! xx' returns 'hi!'. (Like at_word, but right-to-left).
Will need a guard here, as the loop goes right-to-left. The leftmost valid index is 0, so that will figure in the guard test.
See the guide for details Boolean Expression
The code below looks reasonable, but doesn't quite work right
def good_day(age, is_weekend, is_raining): if not is_raining and age < 30 or is_weekend: print('good day')
Because and is higher precedence than or as written above, the code above acts like the following (the and going before the or):
if (not is_raining and age < 30) or is_weekend:
What is a set of data that this code will evaluate incorrectly? raining=True, age=anything, weekend=True .. the or weekend
makes the whole thing True, no matter what the other values are. This does not match the good-day definition above, which requires that it not be raining.
The solution we will spell out is not difficult.
Solution
def good_day(age, is_weekend, is_raining): if not is_raining and (age < 30 or is_weekend): print('good day')
This is operating at a realistic level for parsing data.
at_word99(): Like at-word, but with digits added. We'll say an at-word is an '@' followed by zero or more alphabetic or digit chars. Find and return the alpha-digit part of the first at-word in s, or the empty string if there is none. So 'xx @ab12 xyz' returns 'ab12'.
Like before, but now a word is made of alpha or digit - many real problems will need this sort of code. This may be our most complicated line of code thus far in the quarter! Fortunately, it's a re-usable pattern for any of these "find end of xxxx" problems.
The most difficult part is the "end" loop to locate where the word ends. What is the while test here? (Bring up at_word99() in other window to work it out). We want to use "or" to allow alpha or digit.
at = s.find('@') end = at + 1 while ??????????: end += 1
'@a12' 01234
# 1. Still have the < guard # 2. Use "or" to allow isalpha() or isdigit() # 3. Need to add parens, since this has and+or combination while end < len(s) and (s[end].isalpha() or s[end].isdigit()): end += 1
def at_word99(s): at = s.find('@') if at == -1: return '' # Advance end over alpha or digit chars # use "or" + parens end = at + 1 while end < len(s) and (s[end].isalpha() or s[end].isdigit()): end += 1 word = s[at + 1:end] return word
With the following code, it's clear that the assignment =
sets the variable to point to a value.
x = 7
It's less obvious, but the for loop just sets a variable too, once for each iteration. The variable name is the word the programmer chooses right after the word "for", in this example the variable is i
which is an idiomatic choice:
for i in range(4): # use i print(i)0 1 2 3
The Sartre of Coding!
The variable name is just the label applied to the box that hold the pointer.
You might get the feeling in CS106A to this point: it will only work if the variable is named "i", but that's not true. We named it "i" since that's the idiom, but it's not a requirement.
We try to choose a sensible label to keep our own thoughts organized. However the computer does not care about the word used, so long as the word chosen is used consistently across lines. The variable name i
is idiomatic for that sort of loop. But in reality we could use any variable name, and the code would work exactly the same. Say we name the variable meh
instead .. same output. All that matters is that the variable on line 1 is the same as on line 2.
for meh in range(4): print(meh)0 1 2 3
This is a little disturbing. We do try to choose good and/or idiomatic variable names for our own sake. However, the computer does not notice or care about the actual word choice for our variables.