c8

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.

8.19.2 -- Weird names
...

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)

8.19.3 -- Counting instances of a particular letter in a string
...

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

8.19.4 -- Using the string method find to count occurrences of a letter
...

Now 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

8.19.5 -- Counting words that include "e"
...

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".

8.19.6 -- A pretty multiplication table
...

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)

8.19.7 -- String reversal
...

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

8.19.8 -- "Mirroring" a 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]

8.19.9 -- Remove occurrences of a letter from a string
...

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

8.19.10 -- Palindrome detection
...

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)

8.19.11 -- Count appearances of a substring
...

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

8.19.12 -- Remove a substring once
...

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)

8.19.13 -- Remove all occurrences of a substring
...

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.