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.