Modules and Exceptions

Python modules

Imagine we have a tree_set.py file with the following data in it:

class BSTNode:
    def __init__(self, data, parent=None, left=None, right=None):
        self.data = data
        self.parent = parent
        self.left = left
        self.right = right

    def is_left(self):
        return self.parent and self.parent.left == self

    def is_right(self):
        return self.parent and self.parent.right == self

class BST:
    def __init__(self):
        self.root = None

    def find_at(self, node, value) -> BSTNode:
        if node.data == value:
            return node
        elif value < node.data and node.left:
            return self.find_at(node.left, value)
        elif value > node.data and node.right:
            return self.find_at(node.right, value)
        return None


    def find(self, value) -> BSTNode:
        if not self.root:
            return None
        return self.find_at(self.root, value)

    def add_to(self, node, value):
        if node.data == value:
            return
        elif value < node.data:
            if node.left:
                self.add_to(node.left, value)
            else:
                node.left = BSTNode(value, node)
        else:
            if node.right:
                self.add_to(node.right, value)
            else:
                node.right = BSTNode(value, node)

    def add(self, value):
        if not self.root:
            self.root = BSTNode(value)
        else:
            self.add_to(self.root, value)

    def to_list_at(self, node):
        if not node:
            return []
        left_list = self.to_list_at(node.left)
        here_list = [node.data]
        right_list = self.to_list_at(node.right)
        return left_list + here_list + right_list

    def to_list(self):
        return self.to_list_at(self.root)

    def successor(self, node):
        successor = node.right
        while successor.left:
            successor = successor.left
        return successor

    def replace(self, node, new_node):
        if new_node:
            new_node.parent = node.parent
        if node == self.root:
            self.root = new_node
        elif node.is_left():
            node.parent.left = new_node
        else:
            node.parent.right = new_node

    def remove(self, node):
        # case 1
        if not node.right and not node.left:
            self.replace(node, None)
        # case 2a
        elif not node.right:
            self.replace(node, node.left)
        # case 2b
        elif not node.left:
            self.replace(node, node.right)
        # case 3
        else:
            successor = self.successor(node)
            node.data = successor.data
            self.remove(successor)

class TreeSet:
    def __init__(self):
        self.tree = BST()

    def add(self, item):
        self.tree.add(item)

    def contains(self, value):
        if self.tree.find(value):
            return True
        else:
            return False

    def remove(self, value):
        node = self.tree.find(value)
        if node:
            self.tree.remove(node)

    def count(self):
        # return self.tree.number_of_nodes()
        pass

What if we wanted to write a program that uses a TreeSet? We could:

  • Write the program at the bottom of tree_set.py
  • Copy tree_set.py into the top of our program

Each of these approaches has disadvantages:

  • What if we want to write more than one program that uses tree_set.py?
  • What if we want to change the implementation of tree_set.py?

There’s a better way! We can use tree_set.py as a module in another program.

Let’s write our use_set.py program:

from tree_set import TreeSet

def use_set():
  s = TreeSet()
  s.add(1)
  s.add(2)
  print(s.contains(2))

Let’s look at that from tree_set import TreeSet line. This lets us bring particular parts of tree_set.py into our program. In this case, we’re specifically bringing in the TreeSet class. We can also import functions or variables. When we do so, it’s as if those classes, functions, and variables were defined in our program.

If we evaluate our use_set program, we can run the use_set function.

Defining some terms

Let’s pause for a moment and define some of the terms we’ve been using, just to get everything in one place.

  • A file is a text representation of Python code (such as tree_set.py)
  • A module is a collection of classes, functions, and definitions loaded from a file (such as tree_set).
  • A class is a collection of related methods and data that can be instantiated (such as TreeSet)
  • An object is an instance of a class (such as s = TreeSet())
  • An abstract data type is a paper description of a set of related methods that a class can implement (such as Set)

Exceptions

What if we modified our use_set function as follows:

def use_set():
  s = TreeSet()
  s.add(1)
  s.add("a")
  print(s.contains(2))

What will happen?

We’ll get an error that looks like this:

Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "/Users/dwoos/PycharmProjects/cs0112/use_set.py", line 6, in use_set
    s.add("b")
  File "/Users/dwoos/PycharmProjects/cs0112/tree_set.py", line 101, in add
    self.tree.add(item)
  File "/Users/dwoos/PycharmProjects/cs0112/tree_set.py", line 51, in add
    self.add_to(self.root, value)
  File "/Users/dwoos/PycharmProjects/cs0112/tree_set.py", line 36, in add_to
    elif value < node.data:
TypeError: '<' not supported between instances of 'str' and 'int'

When Python tries to compare "a" to 1, it complains: it can’t compare strings to integers! It then raises an exception, which ends up being shown in the console.

To me, this error message is a bit unclear. I’m not trying to compare a string to an integer–I’m just trying to add some items to a set! We should fix this in the tree_set library. Here’s a first try:

def add(self, item):
  try:
    self.tree.add(item)
  except:
    pass

We’re using a couple of new keywords here: try and except. try introduces a block of code that Python is oging to try to execute. If any exceptions are raised in this block of code, Python runs the corresponding except block.

Right now, our except block will catch any errors that happen and just ignore them. This is probably not actually what we want! If we misspell a variable name in BST.add, we’re never going to find out about it–all of our calls to TreeSet.add will just fail. That’s going to make debugging a real challenge! Next time, we’ll fix this issue.