INTERFACES

Fred Agbo

2026-02-16

Announcements

  • Welcome to week 6!
  • Grading for PS0.1, PS1, & PS2 are all published.
    • Check in with me if you have any questions on the grades/commnets.
  • Week 6 assignment (Problem set 3) is posted and will be due on Wednesday at 10pm

Learning Objectives

  • Develop an interface for a given collection type
  • 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

Developing an Interface

  • An interface defines a set of methods and behaviors that a class must implement, without specifying how these methods are implemented.
  • When you run Python’s help function to obtain information about a module, data type, method, or function,
    • You are accessing documentation about that resource’s interface
  • Interfaces are concise and informative:
    • They allow you to grasp the behavior of a resource
  • In this section, you will develop the interface for a simple collection type called a bag:
    • A bag interface allows clients to use bags effectively and allows implementers to produce new classes that implement this interface

Designing the Bag Interface

  • Derive an interface from thinking about what bags can do in a real-world situation:
    • A bag can contain any objects
  • You will want to know the following:
    • How many things are in a bag
    • How to add things to it or remove them from it
    • Whether a bag is empty
    • How to empty a bag in a single operation
    • How to determine whether a given item is in a bag
    • How to view each item in a bag without emptying it
    • How to create a bag

Designing the Bag Interface

  • The next step is to draw up a list of function names, method names, and operator symbols that meet the descriptions of these operations

  • Examples include:

      - add
      - clear
      - count
      - for ...
      - in
      - isEmpty
      - len
      - remove
      - str
      - +
      - ==

Specifying Arguments and Return Values

  • Next, add arguments to the operations in the interface
    • And think about what values, if any, they return
  • Below, variables b and c refer to bags
b.clear()                   # Make the bag empty
for item in range(10):      # Add 10 numbers to it
    b.add(item)
print(b)                    # Print the contents (a string)
print(4 in b)               # Is 4 in the bag?
print(b.count(2))           # 1 instance of 2 in the bag
c = b + b                   # Contents replicated in a new bag
print(len(c))               # 20 numbers
for item in c:              # Print them all individually
    print(item)
for item in range(10):      # Remove half of them
    c.remove(item)
print(c == b)               #Should be the same contents now

Arguments for Bag Operations and Their Methods

User’s Bag Operation Method in a Bag Class
b = () init(self, sourceCollection=None)
b.add(item) add(self, item)
b.clear() clear(self)
b.count(item) count(self, item)
b.isEmpty() isEmpty(self)
b.remove(item) remove(self, item)
len(b) __len__(self)
str(b) __str__(self)
for item in b __iter__(self).
item in b __contains__(self, item) Not needed if __iter__ is included
b1 + b2 __add__(self, other)
b == anyObject __eq__(self, other)

Constructors and Implementing Classes

  • The first row in table above shows an operation that is the constructor for the particular type of bag being created
  • The syntactic form <class name> is used in the table to indicate that this can be the name of any implementing bag class:
    • The method in the right column is always named _init_
  • An example of code segment that creates an empty linked bag and an array bag that contains the numbers in a given list is as shown:
from arraybag import ArrayBag
from linkedbag import LinkedBag
 
bag1 = LinkedBag()
bag2 = ArrayBag([20, 60, 100, 43])

Constructors and Implementing Classes

  • Final step before expressing an interface in code:
    • describe clearly and concisely what each method does
    • include not only what you expect to occur under normal conditions but also what will happen when something abnormal, such as an error, occurs
  • A more detailed form of documentation can include preconditions and postconditions:
    • Precondition is a statement of what must be true for a method to perform its actions correctly
    • Postcondition states what will be true after the method completes execution, assuming that its preconditions are also true:
      • Usually included in mutator methods, which modify the internal state of an object

Exceptions and Documentation

  • Documentation in an interface should include a statement of any exceptions that could be raised:
    • Example, a bag’s remove method might raise a KeyError if the target item is not in the bag
  • A Python method header for 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."""

Coding an Interface in Python

  • To create an interface, list each of the method headers with its documentation and complete each method with a single pass or return statement:
    • A pass statement is used in the mutator methods that return no value, whereas each accessor method returns a simple default value, such as False, 0, or None
  • So method headers can be checked with the compiler
    • You may place them within a class whose suffix is “Interface”

BagInterface in Python

"""
File: baginterface.py
Author: Ken Lambert
"""
 
class BagInterface(object):
    """Interface for all bag types."""
 
    # Constructor
    def __init__(self, sourceCollection = None):
        """Sets the initial state of self, which includes the
        contents of sourceCollection, if it’s present."""
        pass
 
    # Accessor methods
    def isEmpty(self):
        """Returns True if len(self) == 0,
        or False otherwise."""
        return True
 
    def __len__(self):
        """Returns the number of items in self."""
        return 0
 
    def __str__(self):
        """Returns the string representation of self."""
        return ""
 
    def __iter__(self):
        """Supports iteration over a view of self."""
        return None
    def __add__(self, other):
        """Returns a new bag containing the contents
        of self and other."""
        return None
 
    def __eq__(self, other):
        """Returns True if self equals other,
        or False otherwise."""
        return False
     
    def count(self, item):
        """Returns the number of instances of item in self."""
        return 0
 
    # Mutator methods
    def clear(self):
        """Makes self become empty."""
        pass
 
    def add(self, item):
        """Adds item to self."""
        pass
 
    def remove(self, item):
        """Precondition: item is in self.
        Raises: KeyError if item in not in self.
        Postcondition: item is removed from self."""
        pass

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