Below are exercises from chapter 8 of How to Think Like a Computer Scientist: Learning with Python 3. The text in italics comes from the textbook, and the code (except where it is obviously part of the textbook's prompt) is my solutions.
Modify:
prefixes = "JKLMNOPQ"
suffix = "ack"
for letter in prefixes:
print(letter + suffix)
so that Ouack
and Quack
are spelled correctly.
There are other ways to do this, but one option I like is to add some whitespace and split a string to give us a list of prefixes:
prefixes = "J K L M N Ou P Qu".split()
suffix = "ack"
for prefix in prefixes:
print(prefix + suffix)
Encapsulate
fruit = "banana"
count = 0
for char in fruit:
if char == "a":
count += 1
print(count)
in a function named count_letters
, and generalize it so that it accepts
the string and the letter as arguments. Make the function return the number of
characters, rather than print the answer. The caller should do the printing.
We can do that like so:
def count_letters(ltr, string):
"""Count the number of times a letter ltr appears in a string."""
count = 0
for char in string:
if char == ltr:
count += 1
return count
find
to count occurrences of a letterNow rewrite the count_letters
function so that instead of traversing the
string, it repeatedly calls the find method, with the optional third parameter
to locate new occurrences of the letter being counted.
I was a bit puzzled how an "optional third parameter" would be worked into
this, as the optional third parameter of the find
method is an endpoint for a
search. After a while I figured that the optional third parameter must be an
optional third parameter that I write, and the only way I could think of to
make that work is to make this one of those functions that calls itself. So
here's what I have, though I'm not sure whether this is exactly what the
textbook authors had in mind:
def count_letters2(letter, string, position=0):
"""Count the number of times a letter appears in a string.
Does not count occurrences before position.
"""
new_position = string.find(letter, position)
if new_position != -1:
return 1 + count_letters2(letter, string, new_position + 1)
else:
return 0
Assign to a variable in your program a triple-quoted string that contains your favourite paragraph of text — perhaps a poem, a speech, instructions to bake a cake, some inspirational verses, etc.
Write a function which removes all punctuation from the string, breaks the string into a list of words, and counts the number of words in your text that contain the letter “e”. Your program should print an analysis of the text like this:
Your text contains 243 words, of which 109 (44.8%) contain an "e".
Here's my solution:
def e_routine():
"""Display some statistics.
Takes a preset paragraph and prints statistics about
how many words in it contain the letter 'e'.
"""
paragraph = """Call me Ishmael. Some years ago—never mind how long
precisely—having little or no money in my purse, and nothing
particular to interest me on shore, I thought I would sail about
a little and see the watery part of the world. It is a way I
have of driving off the spleen and regulating the circulation.
Whenever I find myself growing grim about the mouth; whenever it
is a damp, drizzly November in my soul; whenever I find myself
involuntarily pausing before coffin warehouses, and bringing up
the rear of every funeral I meet; and especially whenever my
hypos get such an upper hand of me, that it requires a strong
moral principle to prevent me from deliberately stepping into
the street, and methodically knocking people’s hats off—then, I
account it high time to get to sea as soon as I can. This is my
substitute for pistol and ball. With a philosophical flourish
Cato throws himself upon his sword; I quietly take to the ship.
There is nothing surprising in this. If they but knew it, almost
all men in their degree, some time or other, cherish very nearly
the same feelings towards the ocean with me. """
table = str.maketrans("—", " ", string.punctuation)
paragraph = paragraph.translate(table)
paragraph = paragraph.split()
e_words = 0
for word in paragraph:
if "e" in word:
e_words += 1
total_words = len(paragraph)
e_percent = e_words * 100 / total_words
template = 'Your text contains {0} words, '
'of which {1} ({2:.2f}%) contain an "e".'
print(template.format(total_words, e_words, e_percent))
My version prints:
Your text contains 201 words, of which 78 (38.81%) contain an "e".
Print a neat looking multiplication table like this:
1 2 3 4 5 6 7 8 9 10 11 12
:--------------------------------------------------
1: 1 2 3 4 5 6 7 8 9 10 11 12
2: 2 4 6 8 10 12 14 16 18 20 22 24
3: 3 6 9 12 15 18 21 24 27 30 33 36
4: 4 8 12 16 20 24 28 32 36 40 44 48
5: 5 10 15 20 25 30 35 40 45 50 55 60
6: 6 12 18 24 30 36 42 48 54 60 66 72
7: 7 14 21 28 35 42 49 56 63 70 77 84
8: 8 16 24 32 40 48 56 64 72 80 88 96
9: 9 18 27 36 45 54 63 72 81 90 99 108
10: 10 20 30 40 50 60 70 80 90 100 110 120
11: 11 22 33 44 55 66 77 88 99 110 121 132
12: 12 24 36 48 60 72 84 96 108 120 132 144
Here's some code that can do that.
def print_multiplication_table(n):
"""Print a multiplication table n by n.
Formatting gets all screwed if n >= 32. But why would anyone
want a multiplication table that large?
"""
# make first line
line = " "
for i in range(1, n + 1):
line = line + "{:4}".format(i)
print(line)
# make dividing line
line = " :"
for i in range(n):
line = line + "----"
print(line)
# make the main body line by line
for i in range(1, n+1):
line = "{0:3}:".format(i)
for j in range(1, n + 1):
line = line + "{:4}".format(i*j)
print(line)
Write a function that reverses its string argument, and satisfies these tests:
test(reverse("happy") == "yppah")
test(reverse("Python") == "nohtyP")
test(reverse("") == "")
test(reverse("a") == "a")
We can do this using slice notation:
def reverse(string):
string = string[::-1]
return string
Write a function that mirrors its argument:
test(mirror("good") == "gooddoog")
test(mirror("Python") == "PythonnohtyP")
test(mirror("") == "")
test(mirror("a") == "aa")
{% endhighlight%}
I think the textbook intention may have been to have us rely on our previous
`reverse(string)` function:
```py
def mirror(string):
string = string + reverse(string)
return string
But if we don't want this function to depend on another function, we can just:
def mirror(string):
"""Return a string plus its reverse."""
return string + string[::-1]
Write a function that removes all occurrences of a given letter from a string:
test(remove_letter("a", "apple") == "pple")
test(remove_letter("a", "banana") == "bnn")
test(remove_letter("z", "banana") == "banana")
test(remove_letter("i", "Mississippi") == "Msssspp")
test(remove_letter("b", "") = "")
test(remove_letter("b", "c") = "c")
We can do that like so:
def remove_letter(ltr, string):
"""Remove a letter ltr from a string.
Removes all instances of ltr and returns a new string without it.
"""
new_string = ""
for char in string:
if char != ltr:
new_string = new_string + char
return new_string
Write a function that recognizes palindromes. (Hint: use your reverse
function to make this easy!):
test(is_palindrome("abba"))
test(not is_palindrome("abab"))
test(is_palindrome("tenet"))
test(not is_palindrome("banana"))
test(is_palindrome("straw warts"))
test(is_palindrome("a"))
#test(is_palindrome("")) # Is an empty string a palindrome?
This one's pretty simple:
def is_palindrome(a_string):
"""Return whether a_string is a palindrome."""
return a_string == reverse(a_string)
Write a function that counts how many times a substring occurs in a string:
test(count("is", "Mississippi") == 2)
test(count("an", "banana") == 2)
test(count("ana", "banana") == 2)
test(count("nana", "banana") == 1)
test(count("nanan", "banana") == 0)
test(count("aaa", "aaaaaa") == 4)
We can do this by running the find()
method repeatedly: starting at the
beginning of the string. It will return the first index, if there is one, where
the first instance of the substring starts. Then, starting at the next
character after the index, we'll search for another instance, and so on until
we can't find any more. Throughout, we'll use a counter variable which we bump
each time we find an appearance. That can look like this:
def count(sub_string, string):
"""Count the numbers of times sub_string appears in string."""
appearances = 0
last_found_at = -1
while True:
last_found_at = string.find(sub_string, last_found_at + 1)
if last_found_at == -1:
return appearances
else:
appearances += 1
Write a function that removes the first occurrence of a string from another string:
test(remove("an", "banana") == "bana")
test(remove("cyc", "bicycle") == "bile")
test(remove("iss", "Mississippi") == "Missippi")
test(remove("eggs", "bicycle") == "bicycle")
First, we can find the first instance of the string using find()
, which
returns the index where the substring starts within the string. (If the
substring doesn't occur, we'll just return the original string.) Using the
length of the substring, we can also easily figure out where the substring
ends. Then we can create a new string to output, made up of everything except
the bit we want to remove.
def remove(substring, string):
"""Remove the first instance of a substring from a string.
If the substring does not occur in the string, return the whole
original string.
"""
start = string.find(substring)
if start == -1:
return string
stop = start + len(substring)
output_string = string[0:start] + string[stop:]
return output_string
At least, that's how I solved it, until I did a little Googling, which turned
up a much more efficient way to do it. There's a string method replace(old, new, count)
that we can use within a string to replace a substring old
with
a substring new
, and we can optionally specify that we do it count
number
of times.
Perversely, we can define removing a substring as simply replacing the
substring with an empty string ""
. And because replace()
starts from the
beginning of the string, setting count
to 1 will "replace" the first instance
only. And so we can now rewrite our function with just one functional line:
def remove(substring, string):
"""Remove first instance of substring from string."""
return string.replace(substring, "", 1)
Write a function that removes all occurrences of a string from another string:
test(remove_all("an", "banana") == "ba")
test(remove_all("cyc", "bicycle") == "bile")
test(remove_all("iss", "Mississippi") == "Mippi")
test(remove_all("eggs", "bicycle") == "bicycle")
Using replace()
, we can do this by simply removing count
from our last
function.
def remove_all(substring, string):
"""Remove all instances of substring from string."""
return string.replace(substring, "")
Please, note, though, that although this removes all occurrences of a substring
that can be found in the original string, it does not guarantee that the
removed substring will not occur in the output. That is, if you hand this
function remove_all("na", "bnnaak")
, it will return "bnak"
. If you want to
guarantee the non-occurrence of the substring, you'd need to write a somewhat
different function that does multiple passes.
For that more stringent requirement, we could do something recursive like:
def remove_all_really_thoroughly(substring, string):
"""Remove all instances of substring until none are left."""
next_pass = remove_all(substring, string)
if next_pass == string:
return string
else:
return remove_all_really_thoroughly(substring, next_pass)
That would would, for example, return "bk"
if you gave it
remove_all_really_thoroughly("na", "bnnnnnnnnnaaaaaaaaak")
. I suppose that,
with certain really long string, the recursion could theoretically be
inefficient (I think). But you'd have to cook up some really perverse inputs
before that became relevant.
This page is released under the GNU Free Documentation License, any version 1.3 or later produced by the Free Software Foundation. I am grateful to the authors of the original textbook for releasing it that way: Peter Wentworth, Jeffrey Elkner, Allen B. Downey, and Chris Meyers.