A document from MCS 260 Fall 2021, instructor David Dumas. You can also get the notebook file.

MCS 260 Fall 2021 Worksheet 9 Solutions

  • Course instructor: David Dumas

Topics

This worksheet focuses on dispatch tables, operators on iterables (e.g. any(), all()), and modules.

Problem 1 treated differently

Following the new policy, Problem 1 is different in that:

  • Tuesday lab students: Problem 1 will be presented and solved as a group, in a discussion led by your TA.
  • Thursday lab students: Please attempt problem 1 before coming to lab. Bring a solution, or bring questions. The problem will be discussed as a group.

Resources

The main course materials to refer to for this worksheet are:

(Lecture videos are not linked on worksheets, but are also useful to review while working on worksheets. Video links can be found in the course course Blackboard site.)

1. Refactoring terminal2.py

Recall that we developed a simple terminal application called terminal2.py. Download this program and save it under a new name, terminal3.py.

Then, modify the program as follows:

Move commands to a module

Create a module called termcommands (in a file called termcommands.py) and move all the terminal commands out of terminal3.py and into this file. That means all the functions that begin with do_ should be moved. Also move the dispatch table into the module.

Have terminal3.py import and use this module, so that when you're done it works in the same way as terminal2.py. Test running it, and make sure the commands work.

New command

Add a new command called haspy? (think of that as "has python?") that simply prints "yes" or "no" depending on whether the current directory contains any files whose names end in .py. It should be implemented by a function do_haspy that is in the module termcommands. You'll need to add a new entry to the dispatch table, of course.

If you use list comprehensions and any() in a clever way, you can make do_haspy a one-line function. After you get it working by any method, see if you can achieve a one-line version.

In [ ]:
# Contents of terminal3.py

"""Mini terminal application for demonstration purposes"""
# MCS 260 Fall 2021 Lecture 21, 22, and Workseet 9

# This version of the terminal puts the commands and dispatch
# table in a separate module.

# Note: the os module import from terminal2.py is NOT needed 
# in this version, because os functions are only used in
# termcommands.py

import termcommands

# Main loop

while True:
    # Prompt for input
    s=input("? ")
    cmdparts = s.split()
    if len(cmdparts) == 0:
        print("No command given.")
        continue
    
    name = cmdparts[0]
    args = cmdparts[1:]
    if name in termcommands.handlers:
        try:
            termcommands.handlers[name](*args)
        except TypeError:
            print("Malformed command (wrong number of args?)")
    else:
        termcommands.do_unknown()
In [ ]:
# Contents of termcommands.py

"""Commands and dispatch table for terminal3.py"""
# MCS 260 Fall 2021 Worksheet 9

import os   # needed because many command implementations call os functions!

# COMMANDS

def do_exit():
    """exit the program"""
    exit()

def do_unknown():
    print("Unknown or malformed command.  (The 'help' command will list known commands.)")

def do_help():
    """print a help message"""
    print("Known commands:")
    for name in handlers:
        print(name,"-",handlers[name].__doc__)

def do_whereami():
    """show current working directory"""
    print(os.getcwd())

def do_moveto(a):
    """change current working directory"""
    os.chdir(a)

def do_listdir(*args):
    """list the contents of a directory"""
    if len(args) > 1:
        raise TypeError("listdir accepts at most one argument")
    if len(args) == 1:
        a = args[0]
    else:
        a = os.getcwd()
    for fn in os.listdir(a):
        print(fn)

def do_numfiles(a):
    """show number of files and subdirs in a directory"""
    numfiles = 0
    numdirs = 0   # a might be "C:\Users\ddumas\Dropbox\teaching\mcs260\public\samplecode"
    for fn in os.listdir(a):
        if os.path.isfile(os.path.join(a,fn)):  # fn might be "map1.json"
            numfiles = numfiles + 1
        elif os.path.isdir(os.path.join(a,fn)):
            numdirs = numdirs + 1
    print("{} files\n{} dirs".format(numfiles,numdirs))

def do_create(a):
    """make a new empty file"""
    if os.path.exists(a):
        print("ERROR: {} already exists".format(a))
    else:
        # File does not exist; ok to create
        open(arg,"w").close()

def do_copy(src,dst):
    """copy contents of SRC to new file DST"""
    # copy the file named arg1 to a new file named arg2
    if os.path.exists(dst):
        print("ERROR: Refusing to overwrite existing file")
    else:
        # TODO: Replace with memory-efficient incremental copy
        infile=open(src,"rb")  # "b" means "even if it's not a text file"
        data=infile.read()
        infile.close()
        outfile=open(dst,"wb")
        outfile.write(data)
        outfile.close()

# DISPATCH TABLE

handlers = {
    "help": do_help,
    "exit": do_exit,
    "listdir": do_listdir,
    "numfiles": do_numfiles,
    "create": do_create,
    "moveto": do_moveto,
    "whereami": do_whereami,
    "copy": do_copy
}

2. Comment line zapper

Write a program called commentzapper.py that takes one command line argument, which is expected to be the name of a Python file. It should read that file and print all the code, except it should skip any lines that consist entirely of comments (i.e. no code at all).

For example, suppose that example.py contains the code below:

In [ ]:
# Save this as `example.py` if you want to test the counter program
"Sample of counting"
# MCS 260

def this_line_counts():  # I am a comment on a line that contains code
    "I am a docstring!"
    # TODO: Fix this!
    print("Hello.")

this_line_counts()

Then running

python commentzapper.py example.py

should print the following:

"Sample of counting"

def this_line_counts():  # I am a comment on a line that contains code
    "I am a docstring!"
    print("Hello.")

this_line_counts()

Hint: This is an ideal problem in which to try out Python's continue statement that we learned about last week. If you solve it another way, try to figure out how you might use continue.

In [ ]:
# content of commentzapper.py
# MCS 260 Fall 2021 Worksheet 9
import sys

fobj = open(sys.argv[1],"r",encoding="UTF-8")
for line in fobj:
    stripped = line.strip()
    if len(stripped)>0 and stripped[0] == "#":
        continue
    print(line,end="")

3. Iterable puzzles

Here's a JSON file that contains a list of 100 lists of words.

The word lists have various lengths, and were selected randomly from a large dictionary.

To complete this problem you'll need that file and some code to load it into a variable in Python. Here's an example of code that can do so:

In [1]:
import json

# We assume "iterpuzzles.json" is in the CWD
fobj=open("iterpuzzles.json","r",encoding="UTF-8")
L=json.load(fobj)
fobj.close()

The rest of this problem consists of puzzles that assume you have the data from that file in a variable called L.

Each puzzle asks you to write an expression in Python, ideally one line, that answers the question.

A. Does every list contain a word starting with e?

No:

In [2]:
all( [ any( [w[0]=="e" for w in sublist ]) for sublist in L] )
Out[2]:
False

Optional: Example of a list which has no such words.

In [7]:
L[6]
Out[7]:
['propionitril',
 'microvillous',
 'bimorphs',
 'cuckold',
 'inextensibility',
 'overblessed',
 'oophorocystectomy',
 'baronesses',
 'meaul',
 'drisheen',
 'westralian',
 'seeresses',
 'whirlingly',
 'falser',
 'declass',
 'nonsatirical',
 'foresails',
 'ordinately',
 'preconceal',
 'yentnite',
 'djelfa',
 'compares',
 'meanie',
 'unmethodical',
 'nonscandalously',
 'hesychasm',
 'triskelia',
 'clean',
 'opacity',
 'quangos',
 'strepsipteral',
 'grumbly',
 'nonobjectivism',
 'dispraise',
 'nonzoologic',
 'jibbeh',
 'charismata',
 'bas',
 'unmoble',
 'piano',
 'atmosphered']

B. Does the word "antipathy" appear in any of the lists?

No:

In [8]:
any( ["antipathy" in sublist for sublist in L ] )
Out[8]:
False

Optional extension: All the words in any of the lists starting with "anti", in alphabetical order.

In [16]:
awords=[]
for sublist in L:
    for w in sublist:
        if w not in awords and w[:4] == "anti":
            awords.append(w)
awords.sort()
print(awords)
['antiagglutinant', 'antiaggressively', 'antiantitoxin', 'antiatheist', 'antic', 'anticommunistic', 'anticorona', 'antiexpressively', 'antigalactagogue', 'antiinflammatory', 'antilens', 'antilepsis', 'antilepton', 'antilife', 'antimasquerade', 'antimediaeval', 'antimethodical', 'antineutrinos', 'anting', 'antings', 'antinome', 'antipacifists', 'antiparagraphe', 'antipasto', 'antipodagron', 'antipopulationist', 'antiportable', 'antiproductively', 'antiquer', 'antireservationist', 'antiroll', 'antisnapper', 'antivivisectionists', 'antizealot']

C. How many of the lists contain an odd number of words?

In [18]:
len( [ sublist for sublist in L if len(sublist)%2 ] )
Out[18]:
52

D. For which i is L[i] a list in which the second word begins with the letter a?

In [19]:
[ i for i,sublist in enumerate(L) if L[i][1][0]=="a" ]
Out[19]:
[10, 14, 37, 60, 76, 77, 82, 83, 86, 97]

Optional extension: Let's look at one of these sublists.

In [26]:
L[77]
Out[26]:
['thinnish',
 'argininephosphoric',
 'homomorphous',
 'vindicators',
 'redevote',
 'chieftain']

4. Multiple input of specified types

Write a function multi_input(L) that accepts a list like ["float", "string", "integer", "letter"] and reads corresponding input from the keyboard, returning a list of results. Thus, if given the list above as input, the function might return [ 8.5, "Chicago", 31, "g" ].

Specifically: Each item in the list given as an argument will be a string, and is meant to specify the expected type of one line of input. The possible values are:

  • "float" - indicates a float value is to be read
  • "integer" - indicates an integer is to be read
  • "string" - indicates a string is to be read
  • "letter" - indicates a single letter is to be read

If the actual input does not match the expected type, the function should print a message and try again.

Here is a sample interaction with multi_input( ["float", "string", "integer", "letter"] ):

float: asdf
That didn't work; expected entry of a float.
float: 8.5
string: Chicago
integer: Denver
That didn't work; expected entry of a integer.
integer: 31
letter: jkl
That didn't work; expected entry of a letter.
letter: g

The return value in this case would be [ 8.5, "Chicago", 31, "g" ].

Please use a dispatch table with keys "float", "integer", "string", "letter".

In [28]:
# MCS 260 Fall 2021 Worksheet 9

def single_letter(s):
    """If s is one letter, return it.  Otherwise raise ValueError."""
    if len(s) != 1:
        raise ValueError("Not a single letter: {}".format(s))
    return s
    
converters = {
    "float": float,
    "integer": int,
    "string": str,
    "letter": single_letter,
}

def multi_input(L):
    """Read multiple input values, based on a list of types given in `L`."""
    results = []
    for valtype in L:
        while True:
            s = input(valtype+": ")
            try:
                x = converters[valtype](s)
                break
            except Exception:
                print("That didn't work; expected entry of a {}.".format(valtype))
In [29]:
multi_input( ["float", "string", "integer", "letter"] )
float: asdf
That didn't work; expected entry of a float.
float: 8.5
string: Chicago
integer: Denver
That didn't work; expected entry of a integer.
integer: 31
letter: jkl
That didn't work; expected entry of a letter.
letter: g

Revision history

  • 2021-10-20 Initial release