INTERFACES -2

Fred Agbo

2025-10-01

Announcements

  • Welcome to back!
  • Week 5 assignment (Problem set 2) is due today
  • Week 6-7 assignment and readings will be posted tomorrow

Learning Objectives

  • Implement multiple classes that conform to the interface of a collection type
  • Assess the tradeoffs in running time and memory usage of multiple implementations of a given collection type
  • Implement a basic iterator

Array-Based BagInterface Implementation

  • This section covers developing an array-based implementation of the bag interface, called ArrayBag
  • The design and implementation of the class consists of two steps:
    • Choose an appropriate data structure to contain the collection’s items and determine any other data that might be needed to represent the state of the collection (these data are assigned to instance variables in the __init__ method)
    • Complete the code for the methods specified in the interface

ArrayBag implementation:

  • Step 1: Choose and Initialize the Data Structures
    • An array-based implementation
      • Each object of type ArrayBag contains an array of the items in the bag
      • Can be an instance of the Array class or another array-based collection, such as Python’s list type
    • After initializing the two instance variables, the __init__ method must deal with the possibility that its caller has provided a source collection parameter:
      • If that is the case, all the data in the source collection must be added to the new ArrayBag object

ArrayBag implementation:

  • Step 1: Choose and Initialize the Data Structures

    • The code for this part of the design is easy to create
      • You can make a copy of the bag interface file, baginterface.py, and rename it to arraybag.py
      • You then add an import statement for the array, rename the class to ArrayBag, add a class variable for the default capacity, and complete the __init__ method

ArrayBag implementation:

  • Step 1: Choose and Initialize the Data Structures
"""
File: arraybag.py
"""
 
from arrays import Array
 
class ArrayBag(object):
"""An array-based bag implementation."""
 
    # Class variable
    DEFAULT_CAPACITY = 10
     
    # Constructor
    def __init__(self, sourceCollection = None):
        """Sets the initial state of self, which includes the
        contents of sourceCollection, if it’s present."""
        self.items = Array(ArrayBag.DEFAULT_CAPACITY)
        self.size = 0
        if sourceCollection:
            for item in sourceCollection:
                self.add(item)

Complete the Easy Methods

  • Step 2:
    • Simplest methods in this interface: IsEmpty, _len_, clear, and add
# Accessor methods
def isEmpty(self):
    """Returns True if len(self) == 0, or False otherwise."""
    return len(self) == 0
     
def __len__(self):
    """Returns the number of items in self."""
    return self.size
 
# Mutator methods
def clear(self):
    """Makes self become empty."""
    self.size = 0
    self.items = Array(ArrayBag.DEFAULT_CAPACITY)
 
def add(self, item):
    """Adds item to self."""
    # Check array memory here and increase it if necessary
    self.items[len(self)] = item
    self.size += 1

Complete the Easy Methods

  • Step 2:

    • You should call a method or function on self to get something done within a class definition, whenever possible
    • For example,
      • Whenever you need to use the logical size of the bag within a class definition, run len(self) instead of referring directly to the instance variable self.size
    • The increment of size in the add method
      • Requires you to make the variable the target of the assignment; so you cannot avoid referring to it at that point
    • The add method places the new item at the logical end of the array

Complete the Iterator

  • Step 3:

    • The new __iter__ method maintains a cursor that allows it to navigate through a sequence of objects:
      • The caller’s for loop drives this process
    • On each pass through the calling for loop,
      • The item at the cursor is yielded to the caller, and then the cursor is advanced to the next object in the sequence
      • When the cursor reaches the length of the bag, the __iter__ method’s while loop terminates
      • Which in turn terminates the calling for loop

Complete the Iterator

def __iter__(self):
    """Supports iteration over a view of self."""
    cursor = 0
    while cursor < len(self):
        yield self.items[cursor]
        cursor += 1
  • The yield keyword in Python is used in a function to make it a generator.
  • In this context, yield allows you to use the object in a for loop, and each iteration retrieves the next item from the collection.

Complete the Methods That Use the Iterator

  • Step 3
    • The __eq__ method follows the rules for the equality test discussed in Chapter 2
    • The __add__ method follows the rules for the concatenation of two collections discussed in Chapter 2
    • The __str__ method uses the map and join operations to build a string containing the string representations of the bag’s items

Complete the Methods That Use the Iterator

def __str__(self):
    """Returns the string representation of self."""
    return "{" + ", ".join(map(str, self)) + "}"  
    # possible to use [] to wrap the collection instead of {} which is a set notation
 
def __add__(self, other):
    """Returns a new bag containing the contents
    of self and other."""
    result = ArrayBag(self)
    for item in other:
        result.add(item)
    return result
 
def __eq__(self, other):
    """Returns True if self equals other,
    or False otherwise."""
    if self is other: return True
    if type(self) != type(other) or \
        len(self) != len(other):
        return False
    for item in self:
        if self.count(item) != other.count(item):
            return False
    return True

Complete the remove Method

  • The remove method is the most challenging method to complete in the bag implementation
  • To begin with, you must check the precondition and raise an exception if it is violated
  • Then you must search the underlying array for the target item
  • Finally, you must shift the items in the array to the left to close the hole left by the removed item:
    • Decrement the bag’s size by one
    • Resize the array if necessary

Complete the remove Method

def remove(self, item):
    """Precondition: item is in self.
    Raises: KeyError if item in not in self.
    postcondition: item is removed from self."""
    # 1. check precondition and raise an exception if necessary
    if not item in self:
        raise KeyError(str(item) + " not in bag")
    # 2. Search for index of target item
    targetIndex = 0
    for targetItem in self:
        if targetItem == item:
            break
        targetIndex += 1
    # 3. Shift items to the right of target left by one position
    for i in range(targetIndex, len(self) - 1):
        self.items[i] = self.items[i + 1]
    # 4. Decrement logical size
    self.size -= 1
    # 5. Check array memory here and decrease it if necessary

LinkedBag: Initialize the Data Structures

  • The role of the __init__ method in LinkedBag is to create the instance variables and give them initial values
  • In this case, instead of an array and a logical size, the two pieces of data are a linked structure and a logical size
  • To maintain consistency, you can use the same variable names as before:
    • However, self.items is now an external pointer instead of an array
    • This pointer is initially set to None, the state of an empty linked structure
    • When the structure is not empty, self.items refers to the first node in the linked structure

LinkedBag: Initialize the Data Structures

"""
File: linkedbag.py
"""
from node import Node
 
class LinkedBag(object):
"""A link-based bag implementation."""
 
    # Constructor
    def __init__(self, sourceCollection = None):
        """Sets the initial state of self, which includes the
        contents of sourceCollection, if it’s present."""
        self.items = None
        self.size = 0
        if sourceCollection:
            for item in sourceCollection:
                self.add(item)

LinkedBag: Complete the Iterator

  • The __iter__ method for LinkedBag supports the same kind of traversal as it does in ArrayBag:
    • The logical structure of the two methods is quite similar
def __iter__(self):
    """Supports iteration over a view of self."""
    cursor = self.items
    while cursor != None:
        yield cursor.data
        cursor = cursor.next

LinkedBag: Complete the Methods clear and add

  • The method clear in LinkedBag is quite similar to its sister method in ArrayBag
  • The method add in LinkedBag leverages constant-time access by placing the new item at the head of the linked structure
def add(self, item):
    """Adds item to self."""
    self.items = Node(item, self.items)
    self.size += 1

LinkedBag: Complete the remove Method

  • The method remove in LinkedBag must first handle the precondition and then do a sequential search for the target item
  • When the node containing the target item is found, there are two cases to consider:
    • The target node is at the head of the linked structure, in which case, you must reset variable self.items to this node’s next link
    • The target node is some node after the first one; in which case, the next link of the node before it must be reset to the next link of the target node

LinkedBag: Complete the remove Method

def remove(self, item):
    """Precondition: item is in self.
    Raises: KeyError if item is not in self.
    Postcondition: item is removed from self."""
    # Check precondition and raise an exception if necessary
    if not item in self:
        raise KeyError(str(item) + " not in bag")
    # Search for the node containing the target item
    # probe will point to the target node, and trailer
    # will point to the node before it, if it exists
    probe = self.items
    trailer = None
    for targetItem in self:
        if targetItem == item:
            break
        trailer = probe
        probe = probe.next
    # Unhook the node to be deleted, either the first one or 
    # one thereafter
    if probe == self.items:
        self.items = self.items.next
    else:
        trailer.next = probe.next
    # Decrement logical size
    self.size -= 1

ArrayBag and LinkedBag Performance

  • The running times of the operations on the two implementations of bags are quite similar:
    • The in and remove operations take linear time in both implementations, because they incorporate a sequential search
    • The remove operation in ArrayBag must do the additional work of shifting data items in the array, but the cumulative effect is not worse than linear
    • The +, str, and iter operations are linear
  • The running time of the == operation has several cases and is left as an exercise
  • The remaining operations are constant time:
  • Although ArrayBag’s add incurs an occasional linear time hit to resize the array

ArrayBag and LinkedBag Performance

  • The two implementations have the expected memory tradeoffs
  • When the array within an ArrayBag is better than half full,
    • It uses less memory than a LinkedBag of the same logical size
  • In the worst case,
    • A LinkedBag uses twice as much memory as an ArrayBag whose array is full
  • Because of these memory tradeoffs, removals from an ArrayBag are generally slower than they are on a LinkedBag

Testing the Two Bag Implementations

  • The approach used to test these implementations is to include a tester function for each resource that you develop
  • This function both
    • Ensures that a new collection class conforms to a collection interface
    • Checks to see that these operations do what they are supposed to do

Testing the Two Bag Implementations

  • Code for a standalone application that you can run with any bag class
    • The main function expects a type as an argument and runs the tests on objects of that type

Testing the Two Bag Implementations

"""
File: testbag.py
A tester program for bag implementations.
"""
 
from arraybag import ArrayBag
from linkedbag import LinkedBag
 
def test(bagType):
    """Expects a bag type as an argument and runs some tests
    on objects of that type."""
    print("Testing", bagType)
    lyst = [2013, 61, 1973]
    print("The list of items added is:", lyst)
    b1 = bagType(lyst)
    print("Length, expect 3:", len(b1))
    print("Expect the bag’s string:", b1)
    print("2013 in bag, expect True:", 2013 in b1)
    print("2012 in bag, expect False:", 2012 in b1)
    print("Expect the items on separate lines:")
    for item in b1:
        print(item)
    b1.clear()
    print("Clearing the bag, expect {}:", b1)
    b1.add(25)
    b1.remove(25)
    print("Adding and then removing 25, expect {}:", b1)
    b1 = bagType(lyst)
    b2 = bagType(b1)
    print("Cloning the bag, expect True for ==:", b1 == b2)
    print("Expect False for is:", b1 is b2)
    print("+ the two bags, expect two of each item:", b1 + b2)
    for item in lyst:
        b1.remove(item)
    print("Remove all items, expect {}:", b1)
    print("Removing nonexistent item, expect crash with KeyError:")
    b2.remove(99)

Next week Reading:

  • FDS - Lambert
    • Chapter 7
  • DS&A - John et al.
    • Chapter 4