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

MCS 260 Fall 2021 Worksheet 8 Solutions

  • Course instructor: Emily Dumas

Topics

This worksheet focuses on exceptions, the os module, and the function- and assignment-related concepts from Lecture 20 (multiple return values, tuples, tuple assignment, variadic functions, iterable unpacking).

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. Numbered list of items, boxed or not

If you want to pass multiple items to a function, there are essentially two ways to do it:

  1. Pass them all in one "box", i.e. as a single list or iterable (like f( [1,2,3] ))
  2. Pass each item in its own argument (like f(1, 2, 3))

Imagine we want to make a function numberedprint that prints a numbered list of items. For example, if the items are a, b, and c it might print:

#1: a
#2: b
#3: c

Suppose we want to make this function flexible so that it accepts the items in either of the two ways described above (boxed or not). In other words, you should be able to call it as

numberedprint("a","b","c")

OR

numberedprint( ["a","b","c"] )

and have it produce the same output.

More precisely, the function should do the following:

  • If more than one argument is given, the function should assume that each argument is one of the items
  • If a single argument is given, the function should assume that this argument is an iterable whose elements are the items.
    • However, if an attempt to iterate over the only argument doesn't work, the function should go back to assuming the argument is the (one and only) item.

Hint: Check what kind of exception is raised when you iterate over a non-iterable, like for x in 7:

Solution

In [11]:
def numberedprint(*args):
    """
    Print a numbered list of items, accepting the items as
    separate arguments or as items in a single iterable.
    """

    if len(args)==0:
        # Nothing to do
        return
    
    # Next we'll create an iterable L that contains all the items
    if len(args) > 1:
        # multiple arguments = each arg is an item
        # Just use args itself as the iterable
        L = args
    else:   
        onlyarg = args[0]
        L = []
        try:
            for a in onlyarg:
                L.append(a)
        except TypeError:
            # Oops, the only argument is not actually iterable
            # Interpret it as the only item instead
            L.append(onlyarg)
    
    # Display the results
    for i,a in enumerate(L):
        print("#{}: {}".format(i+1,a))

Checking the solution on the test cases from the worksheet:

In [12]:
numberedprint("a","b","c")  # 3 arguments, so each is an item
#1: a
#2: b
#3: c
In [13]:
numberedprint(True,False) # 2 arguments, so each is an item
#1: True
#2: False
In [14]:
numberedprint( ["a","b","c"] ) # one argument, seen as iterable of items
#1: a
#2: b
#3: c
In [15]:
numberedprint(7) # one argument, but it isn't iterable, so it's considered an item
#1: 7
In [16]:
numberedprint() # no arguments = no output
In [17]:
numberedprint("kangaroo") # string is iterable, so this is viewed as an iterable of items (characters)
#1: k
#2: a
#3: n
#4: g
#5: a
#6: r
#7: o
#8: o
In [18]:
numberedprint("kangaroo","koala") # 2 arguments, so each is an item
#1: kangaroo
#2: koala

Finally, after you have the function working: How can you call it so that it produces this output?

#1: kangaroo

Solution

We pass a list that has the string "kangaroo" as its only element.

In [20]:
numberedprint( ["kangaroo"] )
#1: kangaroo

2. Templates for a semester of homework

Suppose you are taking a class called NDT 371 that gives a homework assignment each week, each of which asks you to write a Python script. For some reason, the problems are always numbered 8 and 9.

Being an enterprising student who is familiar with Python's os module, you decide to make a quick Python script that will generate template Python files for each problem on each homework. That way, when the homework is assigned, you just open the two files and get started.

Create that script. Specifically, it should do the following:

  • Check to see if there is a subdirectory called ndt371 in the current directory.
    • If it does not exist, create a subdirectory with that name.
  • For each homework number N (between 1 and 15) and each problem number P (between 8 and 9), see if a file called hwkNprobP.py exists in the subdirectory ndt371. Here, the N and P should be replaced with the actual numbers, so the filenames will be like hwk7prob8.py and so on.

    • If no file with that name exists, create one (opening it for writing) and write the following text before closing it.

      """DOCSTRING GOES HERE"""
      # NDT371 Fall 2023
      # Firstname Lastname
    • But if a file with that name already exists, the script should leave it alone and print a warning to the terminal, like

      Not creating ndt371/hwk3prob8.py: A file with that name already exists

Notice that this function will behave differently the first time it is run (when it will make all the templates). If you immediately run it again, it should simply warn you that all the templates exist, and not do anything to the files.

To fully test the program, you should run it several times, including under these conditions:

  • Make sure no ndt371 subdirectory exists, and confirm it makes one with all the template Python files
  • Then, delete everything in ndt371 but leave the directory itself, and confirm it still creates the files
  • Then, delete just a few of the files in the directory, and confirm it recreates those while leaving the existing ones untouched

Be careful opening files for writing. This problem specifies behavior where you check to make sure a file does not exist before you open it for writing, to avoid data loss. This problem was purposely formulated to use filenames that probably won't be the same as programs you make for this class, just in case you make a mistake and write a program that clears some files contents.

Solution

In [ ]:
"""
Create templates for a semester of homework
"""
# MCS 260 Fall 2021 Worksheet 8 Problem 2

import os

hwkdir = "ndt371"
template = """\"\"\"DOCSTRING GOES HERE\"\"\"
# NDT371 Fall 2023
# Emily Dumas
"""

if os.path.exists(hwkdir):
    if not os.path.isdir(hwkdir):
        print("{} exists but is not a directory.  Exiting.".format(hwkdir))
        exit()
else:
    os.mkdir(hwkdir)

for N in range(1,16):
    for P in range(8,10):
        fn = os.path.join(hwkdir,"hwk{}prob{}.py".format(N,P))
        if os.path.exists(fn):
            print("Not creating {}: A file with that name already exists".format(fn))
        else:
            fobj = open(fn,"w",encoding="UTF-8")
            fobj.write(template)
            fobj.close()

3. Robust geometric progression tool

Write a script that takes 3 command line arguments, a start value s (a float), a ratio r (a float), and a number of terms N (a nonnegative integer). It should then print the first N terms of a geometric sequence where the first term is s and the ratio of successive terms is r, e.g.

python geomprog.py 2 3 5
2.0
6.0
18.0
54.0
162.0

In this example, the sequence starts at 2, has a ratio of 3, and 5 terms are shown.

However---and this is the key goal of this problem---you should make this program robust against invalid input of all kinds. For example, it should handle all of the following conditions by printing a useful message and exiting normally:

  • Too many command line arguments are given
  • Too few command line arguments are given
  • The number of terms is negative
  • One of the arguments that is supposed to be a float cannot be recognized as such by float()
  • One of the arguments that is supposed to be an int cannot be recognized as such by int()
  • Before completing the requested calculation, the numbers get too large to be represented as Python floats (e.g. s=1, r=123456789, N=50, and notice that the program halts with an error)

Ideally, the result should be a Python script that, regardless of the command line arguments it receives, never exits with an uncaught exception.

Solution

In [ ]:
"""
Print terms in finite geometric progression
"""
# MCS 260 Fall 2021 Worksheet 8 Problem 3

import sys

def usage():
    """
    print a usage message and exit
    """
    print("Usage: {} start ratio num_terms".format(sys.argv[0]))
    print("start and ratio are floats, num_terms must be a nonnegative integer.")
    exit(1)

try:
    # If number of arguments (after script name) is not 3
    # then this will generate ValueError
    s_str, r_str, N_str = sys.argv[1:]
except ValueError:
    print("Wrong number of arguments given.\n")
    usage()

try:
    s = float(s_str)
    r = float(r_str)
    N = int(N_str)
except ValueError:
    print("Invalid argument type(s).\n")
    usage()

if N<0:
    print("num_terms must be nonnegative.\n")
    usage()

try:
    for i in range(N):
        print(s*r**i)
except OverflowError:
    print("Stopped calculation because an overflow occurred.")

Revision history

  • 2021-10-14 Initial release