CS303E Homework 9

Instructor: Dr. Bill Young
Due Date: Tuesday, March 26, 2024 at 11:59pm

Building a List Library

In homework 8, you built a collection of functions to manipulate strings. In this assignment, you'll be doing something very similar but with lists. As with your string library, most of these functions already exist as methods on the list class. However, you should pretend that they don't already exist and define them as functions. Of course, you can't just use the existing methods. This will give you a bunch more practice on manipulating lists and also on defining functions.

Unlike strings, lists are mutable. That is, you can define a list and then change it in place. But in these functions, you'll need to define and return a new list, not just modify the input parameter. Define each required function with the function header as below. The description of each is in the comment supplied.

The only list functions/methods you can use in this assignment are:

  1. indexing (e.g., lst[i], not .index()) and slicing (e.g., lst[i:j], except no slicing for mySlice)
  2. append (i.e., ``+'')
  3. len, in, not in
  4. equality comparison (== or !=))
You can't use any of the other list methods. But you can use other Python programming constructs such as looping over lists (while or for loops) and selection (if statements).

Define each of the following functions, with semantics as indicated by the comment. You do not have to validate the inputs, except as indicated; you can assume that inputs have the indicated types.

At least one question asks you to return two values. That really means returning a tuple (pair) of values. You do that as follows:

   return value1, value2
This actually returns the pair (value1, value2). The caller can then assign the members of the pair to two variables:
   x, y = pairReturningFunction()     # assumes function returns a pair
   z, w = (value1, value2)            # assigning a tuple to 2 variables

If you like, you can use earlier functions in later ones, or define helper functions, though it shouldn't really be necessary. Note that some of these are trivial to write, while others are a bit harder. I have done the first one for you.

BTW: It's strongly suggested that for most of these you consult your solutions to the analogous problems for HW8. You'll find that most have very similar structure, with minor differences.

def myAppend( lst, x ):
    # Return a new list that is like lst but with 
    # the element x at the right end
    return lst + [x]

def myExtend( lst1, lst2 ):
    # Return a new list that contains the elements of
    # lst1 followed by the elements of lst2 in the order
    # given.

def myMax( lst ):
    # Return the element with the highest value.
    # If lst is empty, print "Empty list: no max value"
    # and return None.  You can assume that the list
    # elements can be compared.

def mySum( lst ):
    # Return the sum of the elements in lst.  Assume
    # that the elements are numbers.

def myCount( lst, x ):
    # Return the number of times element x appears
    # in lst.

def myInsert( lst, i, x ):
    # Return a new list like lst except that x has been
    # inserted at the ith position.  I.e., the list is now
    # one element longer than before. Print "Invalid index" if
    # i is negative or is greater than the length of lst and 
    # return None.

def myPop( lst, i ):
    # Return two results: 
    # 1. a new list that is like lst but with the ith 
    #    element removed;
    # 2. the value that was removed.
    # Print "Invalid index" if i is negative or is greater than
    # or equal to len(lst), and return lst unchanged, and None

def myFind( lst, x ):
    # Return the index of the first (leftmost) occurrence of 
    # x in lst, if any.  Return -1 if x does not occur in lst.

def myRFind( lst, x ):
    # Return the index of the last (rightmost) occurrence of 
    # x in lst, if any.  Return -1 if ch does not occur in lst.

def myFindAll( lst, x ):
    # Return a list of indices of occurrences of x in lst, if any.
    # Return the empty list if there are none.

def myReverse( lst ):
    # Return a new list like lst but with the elements
    # in the reverse order. 

def myRemove( lst, x ):
    # Return a new list with the first occurrence of x
    # removed.  If there is none, return lst.

def myRemoveAll( lst, x ):
    # Return a new list with all occurrences of x
    # removed.  If there are none, return lst.

# Don't use slicing for this one:
def mySlice( lst, i, j ):
    # A limited version of the slice operations on lists.
    # If i and j are in [0..len(lst)], return the list 
    # [ lst[i], lst[i+1], ... lst[j-1] ].  I.e., 
    # the slice lst[i:j].  Print an error message if either
    # i or j is not in [0..len(lst)].  Notice that this is 
    # similar but not identical to the way Python slice behaves. 

Expected output:

Below is the output testing the functions I wrote for this assignment:
>>> from MyListFunctions import *
>>> lst1 = ["a", "b", "c"]
>>> lst2 = [1, 2, 3, 4]
>>> lst3 = [1, 2, 3, 1, 2, 3, 1, 2]
>>> myAppend( lst1, "d" )
['a', 'b', 'c', 'd']
>>> myExtend( lst1, lst2 )
['a', 'b', 'c', 1, 2, 3, 4]
>>> myExtend( lst2, lst1 )
[1, 2, 3, 4, 'a', 'b', 'c']
>>> myMax( lst1 )
'c'
>>> myMax( lst2 )
4
>>> mySum( lst2 )
10
>>> myCount( lst1, "e" )
0
>>> myCount( lst1, "a" )
1
>>> myCount( lst3, 3)
2
>>> myInsert( lst1, 0, "d" )
['d', 'a', 'b', 'c']
>>> myInsert( lst1, 2, "e" )
['a', 'b', 'e', 'c']
>>> myInsert( lst1, 7, "e" )
Invalid index
>>> myPop( lst1, 0 )
(['b', 'c'], 'a')
>>> myPop( lst1, 2 )
(['a', 'b'], 'c')
>>> myPop( lst1, 3 )
Invalid index
(['a', 'b', 'c'], None)
>>> myFind( lst2, 1 )
0
>>> myFind( lst2, 3 )
2
>>> myFind( lst2, 5 )
-1
>>> myRFind( lst3, 3 )
5
>>> myRFind( lst3, 1 )
6
>>> myRFind( lst3, 4 )
-1
>>> myFindAll( lst3, 1 )
[0, 3, 6]
>>> myFindAll( lst3, 4 )
[]
>>> myReverse( lst2 )
[4, 3, 2, 1]
>>> myReverse( lst3 )
[2, 1, 3, 2, 1, 3, 2, 1]
>>> myReverse( [1, "a", 2.5] )
[2.5, 'a', 1]
>>> myRemove( lst3, 1 )
[2, 3, 1, 2, 3, 1, 2]
>>> myRemove( lst3, 3 )
[1, 2, 1, 2, 3, 1, 2]
>>> myRemoveAll( lst3, 1 )
[2, 3, 2, 3, 2]
>>> myRemoveAll( lst3, 5 )
[1, 2, 3, 1, 2, 3, 1, 2]
>>> lst = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> mySlice( lst, 0, 5 )
[0, 1, 2, 3, 4]
>>> mySlice( lst, 2, 8 )
[2, 3, 4, 5, 6, 7]
>>> mySlice( lst, -2, 8 )
Illegal index value
>>> mySlice( lst, 0, 11 )
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> mySlice( lst, 0, 12 )
Illegal index value
>>> mySlice( lst, 4, 1 )
[]
>>> mySlice( lst, 3, 3 )
[]
>>> 

Turning in the Assignment:

The program should be in a file named MyListFunctions.py. Submit the file via Canvas before the deadline shown at the top of this page. Submit it to the assignment 10 the assignments sections by uploading your python file.

Your file must compile and run before submission. It must also contain a header with the following format:

# File: MyListFunctions.py
# Student: 
# UT EID:
# Course Name: CS303E
# 
# Date:
# Description of Program: 

Programming Tips:

Write test code: For large team projects, there is often a separate group tasked with writing test code. This typically involves several levels of testing: Unit Testing, Integration Testing, System Testing, and Acceptance Testing. A few years ago, I hired and managed a team of 10 students building test code for a very large web development project for the U.S. Army.

For your own small projects, you don't need anything very complicated. But you should at least do unit testing, meaning to write tests for each function or small unit of code. If you put these into a file, you can just run the file and make sure all tests pass. Then, when you update or change your code, you'll be able to test it very efficiently to make sure that you didn't introduce errors. Each line in the sample output above is effectively a unit test; in a more structured development environment, I'd have put all of them into a file so that I could run the entire suite of tests efficiently. In many development environments, the entire test suite is run automatically periodically, e.g., every night.

Higher order functions: Unlike a lot of programming languages, Python allows what are called higher order functions. That means that the arguments to a function don't have to be objects, but can be other functions. For example, consider the following definition:

def filter( lst, f ):
    # Return a new list that contains exactly the
    # elements of lst that satisfy predicate f.  
    ans = []
    for e in lst:
        if f( e ):
            ans += [e]
    return ans
Notice that the second argument is the name of a function. filter returns the list of all of the elements of the first argument that "satisfy" the predicate (boolean-valued function) in the second argument position. Consider the following predicates:
def isNegative (x):
    # x is a negative number
    return x < 0

def isGreaterThan10 (x):
    # x is a number larger than 10
    return x > 10

def isLCVowel (ch):
    # ch is one of "a", "e", "i", "o", "u"
    return ch in "aeiou"
Now I can filter any list for any predicate I care to define, including those:
>>> from MyListFunctions import *
>>> lst4 = [1, -2, 3, -4, 5, -2.5, 0]
>>> filter( lst4, isNegative )
[-2, -4, -2.5]
>>> filter( lst4, isGreaterThan10 )
[]
>>> filter( [2, 4, 8, 16, 32, 64], isGreaterThan10 )
[16, 32, 64]
>>> filter( list("Who'd have thought it?"), isLCVowel )
['o', 'a', 'e', 'o', 'u', 'i']
Pretty cool, isn't it! Python has several higher order features built in. We're not going to cover them this semester, but they make Python very powerful.